001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.filters;
020
021import com.google.common.collect.Lists;
022import com.puppycrawl.tools.checkstyle.api.AuditEvent;
023import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
024import com.puppycrawl.tools.checkstyle.api.FileContents;
025import com.puppycrawl.tools.checkstyle.api.Filter;
026import com.puppycrawl.tools.checkstyle.api.TextBlock;
027import com.puppycrawl.tools.checkstyle.api.Utils;
028import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder;
029import java.lang.ref.WeakReference;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.List;
033import java.util.regex.Matcher;
034import java.util.regex.Pattern;
035import java.util.regex.PatternSyntaxException;
036import org.apache.commons.beanutils.ConversionException;
037
038/**
039 * <p>
040 * A filter that uses comments to suppress audit events.
041 * </p>
042 * <p>
043 * Rationale:
044 * Sometimes there are legitimate reasons for violating a check.  When
045 * this is a matter of the code in question and not personal
046 * preference, the best place to override the policy is in the code
047 * itself.  Semi-structured comments can be associated with the check.
048 * This is sometimes superior to a separate suppressions file, which
049 * must be kept up-to-date as the source file is edited.
050 * </p>
051 * <p>
052 * Usage:
053 * This check only works in conjunction with the FileContentsHolder module
054 * since that module makes the suppression comments in the .java
055 * files available <i>sub rosa</i>.
056 * </p>
057 * @see FileContentsHolder
058 * @author Mike McMahon
059 * @author Rick Giles
060 */
061public class SuppressionCommentFilter
062    extends AutomaticBean
063    implements Filter
064{
065    /**
066     * A Tag holds a suppression comment and its location, and determines
067     * whether the supression turns checkstyle reporting on or off.
068     * @author Rick Giles
069     */
070    public class Tag
071        implements Comparable<Tag>
072    {
073        /** The text of the tag. */
074        private final String text;
075
076        /** The line number of the tag. */
077        private final int line;
078
079        /** The column number of the tag. */
080        private final int column;
081
082        /** Determines whether the suppression turns checkstyle reporting on. */
083        private final boolean on;
084
085        /** The parsed check regexp, expanded for the text of this tag. */
086        private Pattern tagCheckRegexp;
087
088        /** The parsed message regexp, expanded for the text of this tag. */
089        private Pattern tagMessageRegexp;
090
091        /**
092         * Constructs a tag.
093         * @param line the line number.
094         * @param column the column number.
095         * @param text the text of the suppression.
096         * @param on <code>true</code> if the tag turns checkstyle reporting.
097         * @throws ConversionException if unable to parse expanded text.
098         * on.
099         */
100        public Tag(int line, int column, String text, boolean on)
101            throws ConversionException
102        {
103            this.line = line;
104            this.column = column;
105            this.text = text;
106            this.on = on;
107
108            tagCheckRegexp = checkRegexp;
109            //Expand regexp for check and message
110            //Does not intern Patterns with Utils.getPattern()
111            String format = "";
112            try {
113                if (on) {
114                    format =
115                        expandFromCoont(text, checkFormat, onRegexp);
116                    tagCheckRegexp = Pattern.compile(format);
117                    if (messageFormat != null) {
118                        format =
119                            expandFromCoont(text, messageFormat, onRegexp);
120                        tagMessageRegexp = Pattern.compile(format);
121                    }
122                }
123                else {
124                    format =
125                        expandFromCoont(text, checkFormat, offRegexp);
126                    tagCheckRegexp = Pattern.compile(format);
127                    if (messageFormat != null) {
128                        format =
129                            expandFromCoont(
130                                text,
131                                messageFormat,
132                                offRegexp);
133                        tagMessageRegexp = Pattern.compile(format);
134                    }
135                }
136            }
137            catch (final PatternSyntaxException e) {
138                throw new ConversionException(
139                    "unable to parse expanded comment " + format,
140                    e);
141            }
142        }
143
144        /** @return the text of the tag. */
145        public String getText()
146        {
147            return text;
148        }
149
150        /** @return the line number of the tag in the source file. */
151        public int getLine()
152        {
153            return line;
154        }
155
156        /**
157         * Determines the column number of the tag in the source file.
158         * Will be 0 for all lines of multiline comment, except the
159         * first line.
160         * @return the column number of the tag in the source file.
161         */
162        public int getColumn()
163        {
164            return column;
165        }
166
167        /**
168         * Determines whether the suppression turns checkstyle reporting on or
169         * off.
170         * @return <code>true</code>if the suppression turns reporting on.
171         */
172        public boolean isOn()
173        {
174            return on;
175        }
176
177        /**
178         * Compares the position of this tag in the file
179         * with the position of another tag.
180         * @param object the tag to compare with this one.
181         * @return a negative number if this tag is before the other tag,
182         * 0 if they are at the same position, and a positive number if this
183         * tag is after the other tag.
184         * @see java.lang.Comparable#compareTo(java.lang.Object)
185         */
186        @Override
187        public int compareTo(Tag object)
188        {
189            if (line == object.line) {
190                return column - object.column;
191            }
192
193            return (line - object.line);
194        }
195
196        /**
197         * Determines whether the source of an audit event
198         * matches the text of this tag.
199         * @param event the <code>AuditEvent</code> to check.
200         * @return true if the source of event matches the text of this tag.
201         */
202        public boolean isMatch(AuditEvent event)
203        {
204            final Matcher tagMatcher =
205                tagCheckRegexp.matcher(event.getSourceName());
206            if (tagMatcher.find()) {
207                if (tagMessageRegexp != null) {
208                    final Matcher messageMatcher =
209                            tagMessageRegexp.matcher(event.getMessage());
210                    return messageMatcher.find();
211                }
212                return true;
213            }
214            return false;
215        }
216
217        /**
218         * Expand based on a matching comment.
219         * @param comment the comment.
220         * @param string the string to expand.
221         * @param regexp the parsed expander.
222         * @return the expanded string
223         */
224        private String expandFromCoont(
225            String comment,
226            String string,
227            Pattern regexp)
228        {
229            final Matcher matcher = regexp.matcher(comment);
230            // Match primarily for effect.
231            if (!matcher.find()) {
232                ///CLOVER:OFF
233                return string;
234                ///CLOVER:ON
235            }
236            String result = string;
237            for (int i = 0; i <= matcher.groupCount(); i++) {
238                // $n expands comment match like in Pattern.subst().
239                result = result.replaceAll("\\$" + i, matcher.group(i));
240            }
241            return result;
242        }
243
244        @Override
245        public final String toString()
246        {
247            return "Tag[line=" + getLine() + "; col=" + getColumn()
248                + "; on=" + isOn() + "; text='" + getText() + "']";
249        }
250    }
251
252    /** Turns checkstyle reporting off. */
253    private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE\\:OFF";
254
255    /** Turns checkstyle reporting on. */
256    private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE\\:ON";
257
258    /** Control all checks */
259    private static final String DEFAULT_CHECK_FORMAT = ".*";
260
261    /** Whether to look in comments of the C type. */
262    private boolean checkC = true;
263
264    /** Whether to look in comments of the C++ type. */
265    private boolean checkCPP = true;
266
267    /** Parsed comment regexp that turns checkstyle reporting off. */
268    private Pattern offRegexp;
269
270    /** Parsed comment regexp that turns checkstyle reporting on. */
271    private Pattern onRegexp;
272
273    /** The check format to suppress. */
274    private String checkFormat;
275
276    /** The parsed check regexp. */
277    private Pattern checkRegexp;
278
279    /** The message format to suppress. */
280    private String messageFormat;
281
282    //TODO: Investigate performance improvement with array
283    /** Tagged comments */
284    private final List<Tag> tags = Lists.newArrayList();
285
286    /**
287     * References the current FileContents for this filter.
288     * Since this is a weak reference to the FileContents, the FileContents
289     * can be reclaimed as soon as the strong references in TreeWalker
290     * and FileContentsHolder are reassigned to the next FileContents,
291     * at which time filtering for the current FileContents is finished.
292     */
293    private WeakReference<FileContents> fileContentsReference =
294        new WeakReference<FileContents>(null);
295
296    /**
297     * Constructs a SuppressionCoontFilter.
298     * Initializes comment on, comment off, and check formats
299     * to defaults.
300     */
301    public SuppressionCommentFilter()
302    {
303        setOnCommentFormat(DEFAULT_ON_FORMAT);
304        setOffCommentFormat(DEFAULT_OFF_FORMAT);
305        setCheckFormat(DEFAULT_CHECK_FORMAT);
306    }
307
308    /**
309     * Set the format for a comment that turns off reporting.
310     * @param format a <code>String</code> value.
311     * @throws ConversionException unable to parse format.
312     */
313    public void setOffCommentFormat(String format)
314        throws ConversionException
315    {
316        try {
317            offRegexp = Utils.getPattern(format);
318        }
319        catch (final PatternSyntaxException e) {
320            throw new ConversionException("unable to parse " + format, e);
321        }
322    }
323
324    /**
325     * Set the format for a comment that turns on reporting.
326     * @param format a <code>String</code> value
327     * @throws ConversionException unable to parse format
328     */
329    public void setOnCommentFormat(String format)
330        throws ConversionException
331    {
332        try {
333            onRegexp = Utils.getPattern(format);
334        }
335        catch (final PatternSyntaxException e) {
336            throw new ConversionException("unable to parse " + format, e);
337        }
338    }
339
340    /** @return the FileContents for this filter. */
341    public FileContents getFileContents()
342    {
343        return fileContentsReference.get();
344    }
345
346    /**
347     * Set the FileContents for this filter.
348     * @param fileContents the FileContents for this filter.
349     */
350    public void setFileContents(FileContents fileContents)
351    {
352        fileContentsReference = new WeakReference<FileContents>(fileContents);
353    }
354
355    /**
356     * Set the format for a check.
357     * @param format a <code>String</code> value
358     * @throws ConversionException unable to parse format
359     */
360    public void setCheckFormat(String format)
361        throws ConversionException
362    {
363        try {
364            checkRegexp = Utils.getPattern(format);
365            checkFormat = format;
366        }
367        catch (final PatternSyntaxException e) {
368            throw new ConversionException("unable to parse " + format, e);
369        }
370    }
371
372    /**
373     * Set the format for a message.
374     * @param format a <code>String</code> value
375     * @throws ConversionException unable to parse format
376     */
377    public void setMessageFormat(String format)
378        throws ConversionException
379    {
380        // check that format parses
381        try {
382            Utils.getPattern(format);
383        }
384        catch (final PatternSyntaxException e) {
385            throw new ConversionException("unable to parse " + format, e);
386        }
387        messageFormat = format;
388    }
389
390
391    /**
392     * Set whether to look in C++ comments.
393     * @param checkCPP <code>true</code> if C++ comments are checked.
394     */
395    public void setCheckCPP(boolean checkCPP)
396    {
397        this.checkCPP = checkCPP;
398    }
399
400    /**
401     * Set whether to look in C comments.
402     * @param checkC <code>true</code> if C comments are checked.
403     */
404    public void setCheckC(boolean checkC)
405    {
406        this.checkC = checkC;
407    }
408
409    /** {@inheritDoc} */
410    @Override
411    public boolean accept(AuditEvent event)
412    {
413        if (event.getLocalizedMessage() == null) {
414            return true;        // A special event.
415        }
416
417        // Lazy update. If the first event for the current file, update file
418        // contents and tag suppressions
419        final FileContents currentContents = FileContentsHolder.getContents();
420        if (currentContents == null) {
421            // we have no contents, so we can not filter.
422            // TODO: perhaps we should notify user somehow?
423            return true;
424        }
425        if (getFileContents() != currentContents) {
426            setFileContents(currentContents);
427            tagSuppressions();
428        }
429        final Tag matchTag = findNearestMatch(event);
430        if ((matchTag != null) && !matchTag.isOn()) {
431            return false;
432        }
433        return true;
434    }
435
436    /**
437     * Finds the nearest comment text tag that matches an audit event.
438     * The nearest tag is before the line and column of the event.
439     * @param event the <code>AuditEvent</code> to match.
440     * @return The <code>Tag</code> nearest event.
441     */
442    private Tag findNearestMatch(AuditEvent event)
443    {
444        Tag result = null;
445        // TODO: try binary search if sequential search becomes a performance
446        // problem.
447        for (Tag tag : tags) {
448            if ((tag.getLine() > event.getLine())
449                || ((tag.getLine() == event.getLine())
450                    && (tag.getColumn() > event.getColumn())))
451            {
452                break;
453            }
454            if (tag.isMatch(event)) {
455                result = tag;
456            }
457        };
458        return result;
459    }
460
461    /**
462     * Collects all the suppression tags for all comments into a list and
463     * sorts the list.
464     */
465    private void tagSuppressions()
466    {
467        tags.clear();
468        final FileContents contents = getFileContents();
469        if (checkCPP) {
470            tagSuppressions(contents.getCppComments().values());
471        }
472        if (checkC) {
473            final Collection<List<TextBlock>> cCoonts = contents
474                    .getCComments().values();
475            for (List<TextBlock> eleont : cCoonts) {
476                tagSuppressions(eleont);
477            }
478        }
479        Collections.sort(tags);
480    }
481
482    /**
483     * Appends the suppressions in a collection of comments to the full
484     * set of suppression tags.
485     * @param comments the set of comments.
486     */
487    private void tagSuppressions(Collection<TextBlock> comments)
488    {
489        for (TextBlock comment : comments) {
490            final int startLineNo = comment.getStartLineNo();
491            final String[] text = comment.getText();
492            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
493            for (int i = 1; i < text.length; i++) {
494                tagCommentLine(text[i], startLineNo + i, 0);
495            }
496        }
497    }
498
499    /**
500     * Tags a string if it matches the format for turning
501     * checkstyle reporting on or the format for turning reporting off.
502     * @param text the string to tag.
503     * @param line the line number of text.
504     * @param column the column number of text.
505     */
506    private void tagCommentLine(String text, int line, int column)
507    {
508        final Matcher offMatcher = offRegexp.matcher(text);
509        if (offMatcher.find()) {
510            addTag(offMatcher.group(0), line, column, false);
511        }
512        else {
513            final Matcher onMatcher = onRegexp.matcher(text);
514            if (onMatcher.find()) {
515                addTag(onMatcher.group(0), line, column, true);
516            }
517        }
518    }
519
520    /**
521     * Adds a <code>Tag</code> to the list of all tags.
522     * @param text the text of the tag.
523     * @param line the line number of the tag.
524     * @param column the column number of the tag.
525     * @param on <code>true</code> if the tag turns checkstyle reporting on.
526     */
527    private void addTag(String text, int line, int column, boolean on)
528    {
529        final Tag tag = new Tag(line, column, text, on);
530        tags.add(tag);
531    }
532}