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