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.Iterator;
33  import java.util.List;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  import java.util.regex.PatternSyntaxException;
37  import org.apache.commons.beanutils.ConversionException;
38  
39  /**
40   * <p>
41   * A filter that uses nearby comments to suppress audit events.
42   * </p>
43   * <p>
44   * This check is philosophically similar to {@link SuppressionCommentFilter}.
45   * Unlike {@link SuppressionCommentFilter}, this filter does not require
46   * pairs of comments.  This check may be used to suppress warnings in the
47   * current line:
48   * <pre>
49   *    offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck
50   * </pre>
51   * or it may be configured to span multiple lines, either forward:
52   * <pre>
53   *    // PERMIT MultipleVariableDeclarations NEXT 3 LINES
54   *    double x1 = 1.0, y1 = 0.0, z1 = 0.0;
55   *    double x2 = 0.0, y2 = 1.0, z2 = 0.0;
56   *    double x3 = 0.0, y3 = 0.0, z3 = 1.0;
57   * </pre>
58   * or reverse:
59   * <pre>
60   *   try {
61   *     thirdPartyLibrary.method();
62   *   } catch (RuntimeException e) {
63   *     // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
64   *     // in RuntimeExceptions.
65   *     ...
66   *   }
67   * </pre>
68   *
69   * <p>
70   * See {@link SuppressionCommentFilter} for usage notes.
71   *
72   *
73   * @author Mick Killianey
74   */
75  public class SuppressWithNearbyCommentFilter
76      extends AutomaticBean
77      implements Filter
78  {
79      /**
80       * A Tag holds a suppression comment and its location.
81       */
82      public class Tag implements Comparable<Tag>
83      {
84          /** The text of the tag. */
85          private final String text;
86  
87          /** The first line where warnings may be suppressed. */
88          private int firstLine;
89  
90          /** The last line where warnings may be suppressed. */
91          private int lastLine;
92  
93          /** The parsed check regexp, expanded for the text of this tag. */
94          private Pattern tagCheckRegexp;
95  
96          /** The parsed message regexp, expanded for the text of this tag. */
97          private Pattern tagMessageRegexp;
98  
99          /**
100          * Constructs a tag.
101          * @param text the text of the suppression.
102          * @param line the line number.
103          * @throws ConversionException if unable to parse expanded text.
104          * on.
105          */
106         public Tag(String text, int line)
107             throws ConversionException
108         {
109             this.text = text;
110 
111             tagCheckRegexp = checkRegexp;
112             //Expand regexp for check and message
113             //Does not intern Patterns with Utils.getPattern()
114             String format = "";
115             try {
116                 format = expandFrocomment(text, checkFormat, commentRegexp);
117                 tagCheckRegexp = Pattern.compile(format);
118                 if (messageFormat != null) {
119                     format = expandFrocomment(
120                          text, messageFormat, commentRegexp);
121                     tagMessageRegexp = Pattern.compile(format);
122                 }
123                 int influence = 0;
124                 if (influenceFormat != null) {
125                     format = expandFrocomment(
126                         text, influenceFormat, commentRegexp);
127                     try {
128                         if (format.startsWith("+")) {
129                             format = format.substring(1);
130                         }
131                         influence = Integer.parseInt(format);
132                     }
133                     catch (final NumberFormatException e) {
134                         throw new ConversionException(
135                             "unable to parse influence from '" + text
136                                 + "' using " + influenceFormat, e);
137                     }
138                 }
139                 if (influence >= 0) {
140                     firstLine = line;
141                     lastLine = line + influence;
142                 }
143                 else {
144                     firstLine = line + influence;
145                     lastLine = line;
146                 }
147             }
148             catch (final PatternSyntaxException e) {
149                 throw new ConversionException(
150                     "unable to parse expanded comment " + format,
151                     e);
152             }
153         }
154 
155         /** @return the text of the tag. */
156         public String getText()
157         {
158             return text;
159         }
160 
161         /** @return the line number of the first suppressed line. */
162         public int getFirstLine()
163         {
164             return firstLine;
165         }
166 
167         /** @return the line number of the last suppressed line. */
168         public int getLastLine()
169         {
170             return lastLine;
171         }
172 
173         /**
174          * Compares the position of this tag in the file
175          * with the position of another tag.
176          * @param other the tag to compare with this one.
177          * @return a negative number if this tag is before the other tag,
178          * 0 if they are at the same position, and a positive number if this
179          * tag is after the other tag.
180          * @see java.lang.Comparable#compareTo(java.lang.Object)
181          */
182         @Override
183         public int compareTo(Tag other)
184         {
185             if (firstLine == other.firstLine) {
186                 return lastLine - other.lastLine;
187             }
188 
189             return (firstLine - other.firstLine);
190         }
191 
192         /**
193          * Determines whether the source of an audit event
194          * matches the text of this tag.
195          * @param event the <code>AuditEvent</code> to check.
196          * @return true if the source of event matches the text of this tag.
197          */
198         public boolean isMatch(AuditEvent event)
199         {
200             final int line = event.getLine();
201             if (line < firstLine) {
202                 return false;
203             }
204             if (line > lastLine) {
205                 return false;
206             }
207             final Matcher tagMatcher =
208                 tagCheckRegexp.matcher(event.getSourceName());
209             if (tagMatcher.find()) {
210                 return true;
211             }
212             if (tagMessageRegexp != null) {
213                 final Matcher messageMatcher =
214                     tagMessageRegexp.matcher(event.getMessage());
215                 return messageMatcher.find();
216             }
217             return false;
218         }
219 
220         /**
221          * Expand based on a matching comment.
222          * @param comment the comment.
223          * @param string the string to expand.
224          * @param regexp the parsed expander.
225          * @return the expanded string
226          */
227         private String expandFrocomment(
228             String comment,
229             String string,
230             Pattern regexp)
231         {
232             final Matcher matcher = regexp.matcher(comment);
233             // Match primarily for effect.
234             if (!matcher.find()) {
235                 ///CLOVER:OFF
236                 return string;
237                 ///CLOVER:ON
238             }
239             String result = string;
240             for (int i = 0; i <= matcher.groupCount(); i++) {
241                 // $n expands comment match like in Pattern.subst().
242                 result = result.replaceAll("\\$" + i, matcher.group(i));
243             }
244             return result;
245         }
246 
247         /** {@inheritDoc} */
248         @Override
249         public final String toString()
250         {
251             return "Tag[lines=[" + getFirstLine() + " to " + getLastLine()
252                 + "]; text='" + getText() + "']";
253         }
254     }
255 
256     /** Format to turns checkstyle reporting off. */
257     private static final String DEFAULT_COMMENT_FORMAT =
258         "SUPPRESS CHECKSTYLE (\\w+)";
259 
260     /** Default regex for checks that should be suppressed. */
261     private static final String DEFAULT_CHECK_FORMAT = ".*";
262 
263     /** Default regex for messages that should be suppressed. */
264     private static final String DEFAULT_MESSAGE_FORMAT = null;
265 
266     /** Default regex for lines that should be suppressed. */
267     private static final String DEFAULT_INFLUENCE_FORMAT = "0";
268 
269     /** Whether to look for trigger in C-style comments. */
270     private boolean checkC = true;
271 
272     /** Whether to look for trigger in C++-style comments. */
273     private boolean checkCPP = true;
274 
275     /** Parsed comment regexp that marks checkstyle suppression region. */
276     private Pattern commentRegexp;
277 
278     /** The comment pattern that triggers suppression. */
279     private String checkFormat;
280 
281     /** The parsed check regexp. */
282     private Pattern checkRegexp;
283 
284     /** The message format to suppress. */
285     private String messageFormat;
286 
287     /** The influence of the suppression comment. */
288     private String influenceFormat;
289 
290 
291     //TODO: Investigate performance improvement with array
292     /** Tagged comments */
293     private final List<Tag> tags = Lists.newArrayList();
294 
295     /**
296      * References the current FileContents for this filter.
297      * Since this is a weak reference to the FileContents, the FileContents
298      * can be reclaimed as soon as the strong references in TreeWalker
299      * and FileContentsHolder are reassigned to the next FileContents,
300      * at which time filtering for the current FileContents is finished.
301      */
302     private WeakReference<FileContents> fileContentsReference =
303         new WeakReference<FileContents>(null);
304 
305     /**
306      * Constructs a SuppressionCommentFilter.
307      * Initializes comment on, comment off, and check formats
308      * to defaults.
309      */
310     public SuppressWithNearbyCommentFilter()
311     {
312         if (DEFAULT_COMMENT_FORMAT != null) {
313             setCommentFormat(DEFAULT_COMMENT_FORMAT);
314         }
315         if (DEFAULT_CHECK_FORMAT != null) {
316             setCheckFormat(DEFAULT_CHECK_FORMAT);
317         }
318         if (DEFAULT_MESSAGE_FORMAT != null) {
319             setMessageFormat(DEFAULT_MESSAGE_FORMAT);
320         }
321         if (DEFAULT_INFLUENCE_FORMAT != null) {
322             setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT);
323         }
324     }
325 
326     /**
327      * Set the format for a comment that turns off reporting.
328      * @param format a <code>String</code> value.
329      * @throws ConversionException unable to parse format.
330      */
331     public void setCommentFormat(String format)
332         throws ConversionException
333     {
334         try {
335             commentRegexp = Utils.getPattern(format);
336         }
337         catch (final PatternSyntaxException e) {
338             throw new ConversionException("unable to parse " + format, e);
339         }
340     }
341 
342     /** @return the FileContents for this filter. */
343     public FileContents getFileContents()
344     {
345         return fileContentsReference.get();
346     }
347 
348     /**
349      * Set the FileContents for this filter.
350      * @param fileContents the FileContents for this filter.
351      */
352     public void setFileContents(FileContents fileContents)
353     {
354         fileContentsReference = new WeakReference<FileContents>(fileContents);
355     }
356 
357     /**
358      * Set the format for a check.
359      * @param format a <code>String</code> value
360      * @throws ConversionException unable to parse format
361      */
362     public void setCheckFormat(String format)
363         throws ConversionException
364     {
365         try {
366             checkRegexp = Utils.getPattern(format);
367             checkFormat = format;
368         }
369         catch (final PatternSyntaxException e) {
370             throw new ConversionException("unable to parse " + format, e);
371         }
372     }
373 
374     /**
375      * Set the format for a message.
376      * @param format a <code>String</code> value
377      * @throws ConversionException unable to parse format
378      */
379     public void setMessageFormat(String format)
380         throws ConversionException
381     {
382         // check that format parses
383         try {
384             Utils.getPattern(format);
385         }
386         catch (final PatternSyntaxException e) {
387             throw new ConversionException("unable to parse " + format, e);
388         }
389         messageFormat = format;
390     }
391 
392     /**
393      * Set the format for the influence of this check.
394      * @param format a <code>String</code> value
395      * @throws ConversionException unable to parse format
396      */
397     public void setInfluenceFormat(String format)
398         throws ConversionException
399     {
400         // check that format parses
401         try {
402             Utils.getPattern(format);
403         }
404         catch (final PatternSyntaxException e) {
405             throw new ConversionException("unable to parse " + format, e);
406         }
407         influenceFormat = format;
408     }
409 
410 
411     /**
412      * Set whether to look in C++ comments.
413      * @param checkCPP <code>true</code> if C++ comments are checked.
414      */
415     public void setCheckCPP(boolean checkCPP)
416     {
417         this.checkCPP = checkCPP;
418     }
419 
420     /**
421      * Set whether to look in C comments.
422      * @param checkC <code>true</code> if C comments are checked.
423      */
424     public void setCheckC(boolean checkC)
425     {
426         this.checkC = checkC;
427     }
428 
429     /** {@inheritDoc} */
430     @Override
431     public boolean accept(AuditEvent event)
432     {
433         if (event.getLocalizedMessage() == null) {
434             return true;        // A special event.
435         }
436 
437         // Lazy update. If the first event for the current file, update file
438         // contents and tag suppressions
439         final FileContents currentContents = FileContentsHolder.getContents();
440         if (currentContents == null) {
441             // we have no contents, so we can not filter.
442             // TODO: perhaps we should notify user somehow?
443             return true;
444         }
445         if (getFileContents() != currentContents) {
446             setFileContents(currentContents);
447             tagSuppressions();
448         }
449         for (final Iterator<Tag> iter = tags.iterator(); iter.hasNext();) {
450             final Tag tag = iter.next();
451             if (tag.isMatch(event)) {
452                 return false;
453             }
454         }
455         return true;
456     }
457 
458     /**
459      * Collects all the suppression tags for all comments into a list and
460      * sorts the list.
461      */
462     private void tagSuppressions()
463     {
464         tags.clear();
465         final FileContents contents = getFileContents();
466         if (checkCPP) {
467             tagSuppressions(contents.getCppComments().values());
468         }
469         if (checkC) {
470             final Collection<List<TextBlock>> cComments =
471                 contents.getCComments().values();
472             for (final List<TextBlock> element : cComments) {
473                 tagSuppressions(element);
474             }
475         }
476         Collections.sort(tags);
477     }
478 
479     /**
480      * Appends the suppressions in a collection of comments to the full
481      * set of suppression tags.
482      * @param comments the set of comments.
483      */
484     private void tagSuppressions(Collection<TextBlock> comments)
485     {
486         for (final TextBlock comment : comments) {
487             final int startLineNo = comment.getStartLineNo();
488             final String[] text = comment.getText();
489             tagCommentLine(text[0], startLineNo);
490             for (int i = 1; i < text.length; i++) {
491                 tagCommentLine(text[i], startLineNo + i);
492             }
493         }
494     }
495 
496     /**
497      * Tags a string if it matches the format for turning
498      * checkstyle reporting on or the format for turning reporting off.
499      * @param text the string to tag.
500      * @param line the line number of text.
501      */
502     private void tagCommentLine(String text, int line)
503     {
504         final Matcher matcher = commentRegexp.matcher(text);
505         if (matcher.find()) {
506             addTag(matcher.group(0), line);
507         }
508     }
509 
510     /**
511      * Adds a comment suppression <code>Tag</code> to the list of all tags.
512      * @param text the text of the tag.
513      * @param line the line number of the tag.
514      */
515     private void addTag(String text, int line)
516     {
517         final Tag tag = new Tag(text, line);
518         tags.add(tag);
519     }
520 }