View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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  
20  package com.puppycrawl.tools.checkstyle.filters;
21  
22  import java.lang.ref.WeakReference;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.List;
26  import java.util.Objects;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  import java.util.regex.PatternSyntaxException;
30  
31  import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
32  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
33  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
34  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
35  import com.puppycrawl.tools.checkstyle.api.FileContents;
36  import com.puppycrawl.tools.checkstyle.api.TextBlock;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
38  
39  /**
40   * <p>
41   * A filter that uses nearby comments to suppress audit events.
42   * </p>
43   *
44   * <p>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 ex) {
63   *     // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
64   *     // in RuntimeExceptions.
65   *     ...
66   *   }
67   * </pre>
68   *
69   * <p>See {@link SuppressionCommentFilter} for usage notes.
70   *
71   * @author Mick Killianey
72   */
73  public class SuppressWithNearbyCommentFilter
74      extends AutomaticBean
75      implements TreeWalkerFilter {
76  
77      /** Format to turns checkstyle reporting off. */
78      private static final String DEFAULT_COMMENT_FORMAT =
79          "SUPPRESS CHECKSTYLE (\\w+)";
80  
81      /** Default regex for checks that should be suppressed. */
82      private static final String DEFAULT_CHECK_FORMAT = ".*";
83  
84      /** Default regex for lines that should be suppressed. */
85      private static final String DEFAULT_INFLUENCE_FORMAT = "0";
86  
87      /** Tagged comments. */
88      private final List<Tag> tags = new ArrayList<>();
89  
90      /** Whether to look for trigger in C-style comments. */
91      private boolean checkC = true;
92  
93      /** Whether to look for trigger in C++-style comments. */
94      // -@cs[AbbreviationAsWordInName] We can not change it as,
95      // check's property is a part of API (used in configurations).
96      private boolean checkCPP = true;
97  
98      /** Parsed comment regexp that marks checkstyle suppression region. */
99      private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
100 
101     /** The comment pattern that triggers suppression. */
102     private String checkFormat = DEFAULT_CHECK_FORMAT;
103 
104     /** The message format to suppress. */
105     private String messageFormat;
106 
107     /** The influence of the suppression comment. */
108     private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
109 
110     /**
111      * References the current FileContents for this filter.
112      * Since this is a weak reference to the FileContents, the FileContents
113      * can be reclaimed as soon as the strong references in TreeWalker
114      * are reassigned to the next FileContents, at which time filtering for
115      * the current FileContents is finished.
116      */
117     private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
118 
119     /**
120      * Set the format for a comment that turns off reporting.
121      * @param pattern a pattern.
122      */
123     public final void setCommentFormat(Pattern pattern) {
124         commentFormat = pattern;
125     }
126 
127     /**
128      * Returns FileContents for this filter.
129      * @return the FileContents for this filter.
130      */
131     private FileContents getFileContents() {
132         return fileContentsReference.get();
133     }
134 
135     /**
136      * Set the FileContents for this filter.
137      * @param fileContents the FileContents for this filter.
138      * @noinspection WeakerAccess
139      */
140     public void setFileContents(FileContents fileContents) {
141         fileContentsReference = new WeakReference<>(fileContents);
142     }
143 
144     /**
145      * Set the format for a check.
146      * @param format a {@code String} value
147      */
148     public final void setCheckFormat(String format) {
149         checkFormat = format;
150     }
151 
152     /**
153      * Set the format for a message.
154      * @param format a {@code String} value
155      */
156     public void setMessageFormat(String format) {
157         messageFormat = format;
158     }
159 
160     /**
161      * Set the format for the influence of this check.
162      * @param format a {@code String} value
163      */
164     public final void setInfluenceFormat(String format) {
165         influenceFormat = format;
166     }
167 
168     /**
169      * Set whether to look in C++ comments.
170      * @param checkCpp {@code true} if C++ comments are checked.
171      */
172     // -@cs[AbbreviationAsWordInName] We can not change it as,
173     // check's property is a part of API (used in configurations).
174     public void setCheckCPP(boolean checkCpp) {
175         checkCPP = checkCpp;
176     }
177 
178     /**
179      * Set whether to look in C comments.
180      * @param checkC {@code true} if C comments are checked.
181      */
182     public void setCheckC(boolean checkC) {
183         this.checkC = checkC;
184     }
185 
186     @Override
187     protected void finishLocalSetup() throws CheckstyleException {
188         // No code by default
189     }
190 
191     @Override
192     public boolean accept(TreeWalkerAuditEvent event) {
193         boolean accepted = true;
194 
195         if (event.getLocalizedMessage() != null) {
196             // Lazy update. If the first event for the current file, update file
197             // contents and tag suppressions
198             final FileContents currentContents = event.getFileContents();
199 
200             if (getFileContents() != currentContents) {
201                 setFileContents(currentContents);
202                 tagSuppressions();
203             }
204             if (matchesTag(event)) {
205                 accepted = false;
206             }
207         }
208         return accepted;
209     }
210 
211     /**
212      * Whether current event matches any tag from {@link #tags}.
213      * @param event TreeWalkerAuditEvent to test match on {@link #tags}.
214      * @return true if event matches any tag from {@link #tags}, false otherwise.
215      */
216     private boolean matchesTag(TreeWalkerAuditEvent event) {
217         boolean result = false;
218         for (final Tag tag : tags) {
219             if (tag.isMatch(event)) {
220                 result = true;
221                 break;
222             }
223         }
224         return result;
225     }
226 
227     /**
228      * Collects all the suppression tags for all comments into a list and
229      * sorts the list.
230      */
231     private void tagSuppressions() {
232         tags.clear();
233         final FileContents contents = getFileContents();
234         if (checkCPP) {
235             tagSuppressions(contents.getSingleLineComments().values());
236         }
237         if (checkC) {
238             final Collection<List<TextBlock>> cComments =
239                 contents.getBlockComments().values();
240             cComments.forEach(this::tagSuppressions);
241         }
242     }
243 
244     /**
245      * Appends the suppressions in a collection of comments to the full
246      * set of suppression tags.
247      * @param comments the set of comments.
248      */
249     private void tagSuppressions(Collection<TextBlock> comments) {
250         for (final TextBlock comment : comments) {
251             final int startLineNo = comment.getStartLineNo();
252             final String[] text = comment.getText();
253             tagCommentLine(text[0], startLineNo);
254             for (int i = 1; i < text.length; i++) {
255                 tagCommentLine(text[i], startLineNo + i);
256             }
257         }
258     }
259 
260     /**
261      * Tags a string if it matches the format for turning
262      * checkstyle reporting on or the format for turning reporting off.
263      * @param text the string to tag.
264      * @param line the line number of text.
265      */
266     private void tagCommentLine(String text, int line) {
267         final Matcher matcher = commentFormat.matcher(text);
268         if (matcher.find()) {
269             addTag(matcher.group(0), line);
270         }
271     }
272 
273     /**
274      * Adds a comment suppression {@code Tag} to the list of all tags.
275      * @param text the text of the tag.
276      * @param line the line number of the tag.
277      */
278     private void addTag(String text, int line) {
279         final Tag tag = new Tag(text, line, this);
280         tags.add(tag);
281     }
282 
283     /**
284      * A Tag holds a suppression comment and its location.
285      */
286     public static class Tag {
287         /** The text of the tag. */
288         private final String text;
289 
290         /** The first line where warnings may be suppressed. */
291         private final int firstLine;
292 
293         /** The last line where warnings may be suppressed. */
294         private final int lastLine;
295 
296         /** The parsed check regexp, expanded for the text of this tag. */
297         private final Pattern tagCheckRegexp;
298 
299         /** The parsed message regexp, expanded for the text of this tag. */
300         private final Pattern tagMessageRegexp;
301 
302         /**
303          * Constructs a tag.
304          * @param text the text of the suppression.
305          * @param line the line number.
306          * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
307          * @throws IllegalArgumentException if unable to parse expanded text.
308          */
309         public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
310             this.text = text;
311 
312             //Expand regexp for check and message
313             //Does not intern Patterns with Utils.getPattern()
314             String format = "";
315             try {
316                 format = CommonUtils.fillTemplateWithStringsByRegexp(
317                         filter.checkFormat, text, filter.commentFormat);
318                 tagCheckRegexp = Pattern.compile(format);
319                 if (filter.messageFormat == null) {
320                     tagMessageRegexp = null;
321                 }
322                 else {
323                     format = CommonUtils.fillTemplateWithStringsByRegexp(
324                             filter.messageFormat, text, filter.commentFormat);
325                     tagMessageRegexp = Pattern.compile(format);
326                 }
327                 format = CommonUtils.fillTemplateWithStringsByRegexp(
328                         filter.influenceFormat, text, filter.commentFormat);
329 
330                 if (CommonUtils.startsWithChar(format, '+')) {
331                     format = format.substring(1);
332                 }
333                 final int influence = parseInfluence(format, filter.influenceFormat, text);
334 
335                 if (influence >= 1) {
336                     firstLine = line;
337                     lastLine = line + influence;
338                 }
339                 else {
340                     firstLine = line + influence;
341                     lastLine = line;
342                 }
343             }
344             catch (final PatternSyntaxException ex) {
345                 throw new IllegalArgumentException(
346                     "unable to parse expanded comment " + format, ex);
347             }
348         }
349 
350         /**
351          * Gets influence from suppress filter influence format param.
352          *
353          * @param format          influence format to parse
354          * @param influenceFormat raw influence format
355          * @param text            text of the suppression
356          * @return parsed influence
357          */
358         private static int parseInfluence(String format, String influenceFormat, String text) {
359             try {
360                 return Integer.parseInt(format);
361             }
362             catch (final NumberFormatException ex) {
363                 throw new IllegalArgumentException("unable to parse influence from '" + text
364                         + "' using " + influenceFormat, ex);
365             }
366         }
367 
368         @Override
369         public boolean equals(Object other) {
370             if (this == other) {
371                 return true;
372             }
373             if (other == null || getClass() != other.getClass()) {
374                 return false;
375             }
376             final Tag tag = (Tag) other;
377             return Objects.equals(firstLine, tag.firstLine)
378                     && Objects.equals(lastLine, tag.lastLine)
379                     && Objects.equals(text, tag.text)
380                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
381                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
382         }
383 
384         @Override
385         public int hashCode() {
386             return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp);
387         }
388 
389         /**
390          * Determines whether the source of an audit event
391          * matches the text of this tag.
392          * @param event the {@code TreeWalkerAuditEvent} to check.
393          * @return true if the source of event matches the text of this tag.
394          */
395         public boolean isMatch(TreeWalkerAuditEvent event) {
396             final int line = event.getLine();
397             boolean match = false;
398 
399             if (line >= firstLine && line <= lastLine) {
400                 final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
401 
402                 if (tagMatcher.find()) {
403                     match = true;
404                 }
405                 else if (tagMessageRegexp == null) {
406                     if (event.getModuleId() != null) {
407                         final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
408                         match = idMatcher.find();
409                     }
410                 }
411                 else {
412                     final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
413                     match = messageMatcher.find();
414                 }
415             }
416             return match;
417         }
418 
419         @Override
420         public String toString() {
421             return "Tag[text='" + text + '\''
422                     + ", firstLine=" + firstLine
423                     + ", lastLine=" + lastLine
424                     + ", tagCheckRegexp=" + tagCheckRegexp
425                     + ", tagMessageRegexp=" + tagMessageRegexp
426                     + ']';
427         }
428     }
429 }