View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2014  Oliver Burn
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 =
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 }