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.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 = new WeakReference<>(null);
303 
304     /**
305      * Constructs a SuppressionCommentFilter.
306      * Initializes comment on, comment off, and check formats
307      * to defaults.
308      */
309     public SuppressWithNearbyCommentFilter()
310     {
311         if (DEFAULT_COMMENT_FORMAT != null) {
312             setCommentFormat(DEFAULT_COMMENT_FORMAT);
313         }
314         if (DEFAULT_CHECK_FORMAT != null) {
315             setCheckFormat(DEFAULT_CHECK_FORMAT);
316         }
317         if (DEFAULT_MESSAGE_FORMAT != null) {
318             setMessageFormat(DEFAULT_MESSAGE_FORMAT);
319         }
320         if (DEFAULT_INFLUENCE_FORMAT != null) {
321             setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT);
322         }
323     }
324 
325     /**
326      * Set the format for a comment that turns off reporting.
327      * @param format a <code>String</code> value.
328      * @throws ConversionException unable to parse format.
329      */
330     public void setCommentFormat(String format)
331         throws ConversionException
332     {
333         try {
334             commentRegexp = Utils.getPattern(format);
335         }
336         catch (final PatternSyntaxException e) {
337             throw new ConversionException("unable to parse " + format, e);
338         }
339     }
340 
341     /** @return the FileContents for this filter. */
342     public FileContents getFileContents()
343     {
344         return fileContentsReference.get();
345     }
346 
347     /**
348      * Set the FileContents for this filter.
349      * @param fileContents the FileContents for this filter.
350      */
351     public void setFileContents(FileContents fileContents)
352     {
353         fileContentsReference = new WeakReference<>(fileContents);
354     }
355 
356     /**
357      * Set the format for a check.
358      * @param format a <code>String</code> value
359      * @throws ConversionException unable to parse format
360      */
361     public void setCheckFormat(String format)
362         throws ConversionException
363     {
364         try {
365             checkRegexp = Utils.getPattern(format);
366             checkFormat = format;
367         }
368         catch (final PatternSyntaxException e) {
369             throw new ConversionException("unable to parse " + format, e);
370         }
371     }
372 
373     /**
374      * Set the format for a message.
375      * @param format a <code>String</code> value
376      * @throws ConversionException unable to parse format
377      */
378     public void setMessageFormat(String format)
379         throws ConversionException
380     {
381         // check that format parses
382         try {
383             Utils.getPattern(format);
384         }
385         catch (final PatternSyntaxException e) {
386             throw new ConversionException("unable to parse " + format, e);
387         }
388         messageFormat = format;
389     }
390 
391     /**
392      * Set the format for the influence of this check.
393      * @param format a <code>String</code> value
394      * @throws ConversionException unable to parse format
395      */
396     public void setInfluenceFormat(String format)
397         throws ConversionException
398     {
399         // check that format parses
400         try {
401             Utils.getPattern(format);
402         }
403         catch (final PatternSyntaxException e) {
404             throw new ConversionException("unable to parse " + format, e);
405         }
406         influenceFormat = format;
407     }
408 
409 
410     /**
411      * Set whether to look in C++ comments.
412      * @param checkCPP <code>true</code> if C++ comments are checked.
413      */
414     public void setCheckCPP(boolean checkCPP)
415     {
416         this.checkCPP = checkCPP;
417     }
418 
419     /**
420      * Set whether to look in C comments.
421      * @param checkC <code>true</code> if C comments are checked.
422      */
423     public void setCheckC(boolean checkC)
424     {
425         this.checkC = checkC;
426     }
427 
428     /** {@inheritDoc} */
429     @Override
430     public boolean accept(AuditEvent event)
431     {
432         if (event.getLocalizedMessage() == null) {
433             return true;        // A special event.
434         }
435 
436         // Lazy update. If the first event for the current file, update file
437         // contents and tag suppressions
438         final FileContents currentContents = FileContentsHolder.getContents();
439         if (currentContents == null) {
440             // we have no contents, so we can not filter.
441             // TODO: perhaps we should notify user somehow?
442             return true;
443         }
444         if (getFileContents() != currentContents) {
445             setFileContents(currentContents);
446             tagSuppressions();
447         }
448         for (final Iterator<Tag> iter = tags.iterator(); iter.hasNext();) {
449             final Tag tag = iter.next();
450             if (tag.isMatch(event)) {
451                 return false;
452             }
453         }
454         return true;
455     }
456 
457     /**
458      * Collects all the suppression tags for all comments into a list and
459      * sorts the list.
460      */
461     private void tagSuppressions()
462     {
463         tags.clear();
464         final FileContents contents = getFileContents();
465         if (checkCPP) {
466             tagSuppressions(contents.getCppComments().values());
467         }
468         if (checkC) {
469             final Collection<List<TextBlock>> cComments =
470                 contents.getCComments().values();
471             for (final List<TextBlock> element : cComments) {
472                 tagSuppressions(element);
473             }
474         }
475         Collections.sort(tags);
476     }
477 
478     /**
479      * Appends the suppressions in a collection of comments to the full
480      * set of suppression tags.
481      * @param comments the set of comments.
482      */
483     private void tagSuppressions(Collection<TextBlock> comments)
484     {
485         for (final TextBlock comment : comments) {
486             final int startLineNo = comment.getStartLineNo();
487             final String[] text = comment.getText();
488             tagCommentLine(text[0], startLineNo);
489             for (int i = 1; i < text.length; i++) {
490                 tagCommentLine(text[i], startLineNo + i);
491             }
492         }
493     }
494 
495     /**
496      * Tags a string if it matches the format for turning
497      * checkstyle reporting on or the format for turning reporting off.
498      * @param text the string to tag.
499      * @param line the line number of text.
500      */
501     private void tagCommentLine(String text, int line)
502     {
503         final Matcher matcher = commentRegexp.matcher(text);
504         if (matcher.find()) {
505             addTag(matcher.group(0), line);
506         }
507     }
508 
509     /**
510      * Adds a comment suppression <code>Tag</code> to the list of all tags.
511      * @param text the text of the tag.
512      * @param line the line number of the tag.
513      */
514     private void addTag(String text, int line)
515     {
516         final Tag tag = new Tag(text, line);
517         tags.add(tag);
518     }
519 }