View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2015 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  package com.puppycrawl.tools.checkstyle.filters;
20  
21  import com.google.common.collect.Lists;
22  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
23  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
24  import com.puppycrawl.tools.checkstyle.api.FileContents;
25  import com.puppycrawl.tools.checkstyle.api.Filter;
26  import com.puppycrawl.tools.checkstyle.api.TextBlock;
27  import com.puppycrawl.tools.checkstyle.api.Utils;
28  import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder;
29  import java.lang.ref.WeakReference;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.List;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  import java.util.regex.PatternSyntaxException;
36  import org.apache.commons.beanutils.ConversionException;
37  
38  /**
39   * <p>
40   * A filter that uses comments to suppress audit events.
41   * </p>
42   * <p>
43   * Rationale:
44   * Sometimes there are legitimate reasons for violating a check.  When
45   * this is a matter of the code in question and not personal
46   * preference, the best place to override the policy is in the code
47   * itself.  Semi-structured comments can be associated with the check.
48   * This is sometimes superior to a separate suppressions file, which
49   * must be kept up-to-date as the source file is edited.
50   * </p>
51   * <p>
52   * Usage:
53   * This check only works in conjunction with the FileContentsHolder module
54   * since that module makes the suppression comments in the .java
55   * files available <i>sub rosa</i>.
56   * </p>
57   * @see FileContentsHolder
58   * @author Mike McMahon
59   * @author Rick Giles
60   */
61  public class SuppressionCommentFilter
62      extends AutomaticBean
63      implements Filter
64  {
65      /**
66       * A Tag holds a suppression comment and its location, and determines
67       * whether the supression turns checkstyle reporting on or off.
68       * @author Rick Giles
69       */
70      public class Tag
71          implements Comparable<Tag>
72      {
73          /** The text of the tag. */
74          private final String text;
75  
76          /** The line number of the tag. */
77          private final int line;
78  
79          /** The column number of the tag. */
80          private final int column;
81  
82          /** Determines whether the suppression turns checkstyle reporting on. */
83          private final boolean on;
84  
85          /** The parsed check regexp, expanded for the text of this tag. */
86          private Pattern tagCheckRegexp;
87  
88          /** The parsed message regexp, expanded for the text of this tag. */
89          private Pattern tagMessageRegexp;
90  
91          /**
92           * Constructs a tag.
93           * @param line the line number.
94           * @param column the column number.
95           * @param text the text of the suppression.
96           * @param on <code>true</code> if the tag turns checkstyle reporting.
97           * @throws ConversionException if unable to parse expanded text.
98           * on.
99           */
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 = new WeakReference<>(null);
294 
295     /**
296      * Constructs a SuppressionCoontFilter.
297      * Initializes comment on, comment off, and check formats
298      * to defaults.
299      */
300     public SuppressionCommentFilter()
301     {
302         setOnCommentFormat(DEFAULT_ON_FORMAT);
303         setOffCommentFormat(DEFAULT_OFF_FORMAT);
304         setCheckFormat(DEFAULT_CHECK_FORMAT);
305     }
306 
307     /**
308      * Set the format for a comment that turns off reporting.
309      * @param format a <code>String</code> value.
310      * @throws ConversionException unable to parse format.
311      */
312     public void setOffCommentFormat(String format)
313         throws ConversionException
314     {
315         try {
316             offRegexp = Utils.getPattern(format);
317         }
318         catch (final PatternSyntaxException e) {
319             throw new ConversionException("unable to parse " + format, e);
320         }
321     }
322 
323     /**
324      * Set the format for a comment that turns on reporting.
325      * @param format a <code>String</code> value
326      * @throws ConversionException unable to parse format
327      */
328     public void setOnCommentFormat(String format)
329         throws ConversionException
330     {
331         try {
332             onRegexp = Utils.getPattern(format);
333         }
334         catch (final PatternSyntaxException e) {
335             throw new ConversionException("unable to parse " + format, e);
336         }
337     }
338 
339     /** @return the FileContents for this filter. */
340     public FileContents getFileContents()
341     {
342         return fileContentsReference.get();
343     }
344 
345     /**
346      * Set the FileContents for this filter.
347      * @param fileContents the FileContents for this filter.
348      */
349     public void setFileContents(FileContents fileContents)
350     {
351         fileContentsReference = new WeakReference<>(fileContents);
352     }
353 
354     /**
355      * Set the format for a check.
356      * @param format a <code>String</code> value
357      * @throws ConversionException unable to parse format
358      */
359     public void setCheckFormat(String format)
360         throws ConversionException
361     {
362         try {
363             checkRegexp = Utils.getPattern(format);
364             checkFormat = format;
365         }
366         catch (final PatternSyntaxException e) {
367             throw new ConversionException("unable to parse " + format, e);
368         }
369     }
370 
371     /**
372      * Set the format for a message.
373      * @param format a <code>String</code> value
374      * @throws ConversionException unable to parse format
375      */
376     public void setMessageFormat(String format)
377         throws ConversionException
378     {
379         // check that format parses
380         try {
381             Utils.getPattern(format);
382         }
383         catch (final PatternSyntaxException e) {
384             throw new ConversionException("unable to parse " + format, e);
385         }
386         messageFormat = format;
387     }
388 
389 
390     /**
391      * Set whether to look in C++ comments.
392      * @param checkCPP <code>true</code> if C++ comments are checked.
393      */
394     public void setCheckCPP(boolean checkCPP)
395     {
396         this.checkCPP = checkCPP;
397     }
398 
399     /**
400      * Set whether to look in C comments.
401      * @param checkC <code>true</code> if C comments are checked.
402      */
403     public void setCheckC(boolean checkC)
404     {
405         this.checkC = checkC;
406     }
407 
408     /** {@inheritDoc} */
409     @Override
410     public boolean accept(AuditEvent event)
411     {
412         if (event.getLocalizedMessage() == null) {
413             return true;        // A special event.
414         }
415 
416         // Lazy update. If the first event for the current file, update file
417         // contents and tag suppressions
418         final FileContents currentContents = FileContentsHolder.getContents();
419         if (currentContents == null) {
420             // we have no contents, so we can not filter.
421             // TODO: perhaps we should notify user somehow?
422             return true;
423         }
424         if (getFileContents() != currentContents) {
425             setFileContents(currentContents);
426             tagSuppressions();
427         }
428         final Tag matchTag = findNearestMatch(event);
429         if ((matchTag != null) && !matchTag.isOn()) {
430             return false;
431         }
432         return true;
433     }
434 
435     /**
436      * Finds the nearest comment text tag that matches an audit event.
437      * The nearest tag is before the line and column of the event.
438      * @param event the <code>AuditEvent</code> to match.
439      * @return The <code>Tag</code> nearest event.
440      */
441     private Tag findNearestMatch(AuditEvent event)
442     {
443         Tag result = null;
444         // TODO: try binary search if sequential search becomes a performance
445         // problem.
446         for (Tag tag : tags) {
447             if ((tag.getLine() > event.getLine())
448                 || ((tag.getLine() == event.getLine())
449                     && (tag.getColumn() > event.getColumn())))
450             {
451                 break;
452             }
453             if (tag.isMatch(event)) {
454                 result = tag;
455             }
456         };
457         return result;
458     }
459 
460     /**
461      * Collects all the suppression tags for all comments into a list and
462      * sorts the list.
463      */
464     private void tagSuppressions()
465     {
466         tags.clear();
467         final FileContents contents = getFileContents();
468         if (checkCPP) {
469             tagSuppressions(contents.getCppComments().values());
470         }
471         if (checkC) {
472             final Collection<List<TextBlock>> cCoonts = contents
473                     .getCComments().values();
474             for (List<TextBlock> eleont : cCoonts) {
475                 tagSuppressions(eleont);
476             }
477         }
478         Collections.sort(tags);
479     }
480 
481     /**
482      * Appends the suppressions in a collection of comments to the full
483      * set of suppression tags.
484      * @param comments the set of comments.
485      */
486     private void tagSuppressions(Collection<TextBlock> comments)
487     {
488         for (TextBlock comment : comments) {
489             final int startLineNo = comment.getStartLineNo();
490             final String[] text = comment.getText();
491             tagCommentLine(text[0], startLineNo, comment.getStartColNo());
492             for (int i = 1; i < text.length; i++) {
493                 tagCommentLine(text[i], startLineNo + i, 0);
494             }
495         }
496     }
497 
498     /**
499      * Tags a string if it matches the format for turning
500      * checkstyle reporting on or the format for turning reporting off.
501      * @param text the string to tag.
502      * @param line the line number of text.
503      * @param column the column number of text.
504      */
505     private void tagCommentLine(String text, int line, int column)
506     {
507         final Matcher offMatcher = offRegexp.matcher(text);
508         if (offMatcher.find()) {
509             addTag(offMatcher.group(0), line, column, false);
510         }
511         else {
512             final Matcher onMatcher = onRegexp.matcher(text);
513             if (onMatcher.find()) {
514                 addTag(onMatcher.group(0), line, column, true);
515             }
516         }
517     }
518 
519     /**
520      * Adds a <code>Tag</code> to the list of all tags.
521      * @param text the text of the tag.
522      * @param line the line number of the tag.
523      * @param column the column number of the tag.
524      * @param on <code>true</code> if the tag turns checkstyle reporting on.
525      */
526     private void addTag(String text, int line, int column, boolean on)
527     {
528         final Tag tag = new Tag(line, column, text, on);
529         tags.add(tag);
530     }
531 }