View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.AbstractAutomaticBean;
32  import com.puppycrawl.tools.checkstyle.PropertyType;
33  import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
34  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
35  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
36  import com.puppycrawl.tools.checkstyle.api.FileContents;
37  import com.puppycrawl.tools.checkstyle.api.TextBlock;
38  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39  
40  /**
41   * <p>
42   * Filter {@code SuppressWithNearbyCommentFilter} uses nearby comments to suppress audit events.
43   * </p>
44   * <p>
45   * Rationale: Same as {@code SuppressionCommentFilter}.
46   * Whereas the SuppressionCommentFilter uses matched pairs of filters to turn
47   * on/off comment matching, {@code SuppressWithNearbyCommentFilter} uses single comments.
48   * This requires fewer lines to mark a region, and may be aesthetically preferable in some contexts.
49   * </p>
50   * <p>
51   * Attention: This filter may only be specified within the TreeWalker module
52   * ({@code &lt;module name="TreeWalker"/&gt;}) and only applies to checks which are also
53   * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline},
54   * a
55   * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html#SuppressWithPlainTextCommentFilter">
56   * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
57   * </p>
58   * <p>
59   * SuppressWithNearbyCommentFilter can suppress Checks that have
60   * Treewalker as parent module.
61   * </p>
62   * <ul>
63   * <li>
64   * Property {@code checkC} - Control whether to check C style comments ({@code &#47;* ... *&#47;}).
65   * Type is {@code boolean}.
66   * Default value is {@code true}.
67   * </li>
68   * <li>
69   * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}).
70   * Type is {@code boolean}.
71   * Default value is {@code true}.
72   * </li>
73   * <li>
74   * Property {@code checkFormat} - Specify check pattern to suppress.
75   * Type is {@code java.util.regex.Pattern}.
76   * Default value is {@code ".*"}.
77   * </li>
78   * <li>
79   * Property {@code commentFormat} - Specify comment pattern to trigger filter to begin suppression.
80   * Type is {@code java.util.regex.Pattern}.
81   * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}.
82   * </li>
83   * <li>
84   * Property {@code idFormat} - Specify check ID pattern to suppress.
85   * Type is {@code java.util.regex.Pattern}.
86   * Default value is {@code null}.
87   * </li>
88   * <li>
89   * Property {@code influenceFormat} - Specify negative/zero/positive value that
90   * defines the number of lines preceding/at/following the suppression comment.
91   * Type is {@code java.lang.String}.
92   * Default value is {@code "0"}.
93   * </li>
94   * <li>
95   * Property {@code messageFormat} - Define message pattern to suppress.
96   * Type is {@code java.util.regex.Pattern}.
97   * Default value is {@code null}.
98   * </li>
99   * </ul>
100  * <p>
101  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
102  * </p>
103  *
104  * @since 5.0
105  */
106 public class SuppressWithNearbyCommentFilter
107     extends AbstractAutomaticBean
108     implements TreeWalkerFilter {
109 
110     /** Format to turn checkstyle reporting off. */
111     private static final String DEFAULT_COMMENT_FORMAT =
112         "SUPPRESS CHECKSTYLE (\\w+)";
113 
114     /** Default regex for checks that should be suppressed. */
115     private static final String DEFAULT_CHECK_FORMAT = ".*";
116 
117     /** Default regex for lines that should be suppressed. */
118     private static final String DEFAULT_INFLUENCE_FORMAT = "0";
119 
120     /** Tagged comments. */
121     private final List<Tag> tags = new ArrayList<>();
122 
123     /** Control whether to check C style comments ({@code &#47;* ... *&#47;}). */
124     private boolean checkC = true;
125 
126     /** Control whether to check C++ style comments ({@code //}). */
127     // -@cs[AbbreviationAsWordInName] We can not change it as,
128     // check's property is a part of API (used in configurations).
129     private boolean checkCPP = true;
130 
131     /** Specify comment pattern to trigger filter to begin suppression. */
132     private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
133 
134     /** Specify check pattern to suppress. */
135     @XdocsPropertyType(PropertyType.PATTERN)
136     private String checkFormat = DEFAULT_CHECK_FORMAT;
137 
138     /** Define message pattern to suppress. */
139     @XdocsPropertyType(PropertyType.PATTERN)
140     private String messageFormat;
141 
142     /** Specify check ID pattern to suppress. */
143     @XdocsPropertyType(PropertyType.PATTERN)
144     private String idFormat;
145 
146     /**
147      * Specify negative/zero/positive value that defines the number of lines
148      * preceding/at/following the suppression comment.
149      */
150     private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
151 
152     /**
153      * References the current FileContents for this filter.
154      * Since this is a weak reference to the FileContents, the FileContents
155      * can be reclaimed as soon as the strong references in TreeWalker
156      * are reassigned to the next FileContents, at which time filtering for
157      * the current FileContents is finished.
158      */
159     private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
160 
161     /**
162      * Setter to specify comment pattern to trigger filter to begin suppression.
163      *
164      * @param pattern a pattern.
165      * @since 5.0
166      */
167     public final void setCommentFormat(Pattern pattern) {
168         commentFormat = pattern;
169     }
170 
171     /**
172      * Returns FileContents for this filter.
173      *
174      * @return the FileContents for this filter.
175      */
176     private FileContents getFileContents() {
177         return fileContentsReference.get();
178     }
179 
180     /**
181      * Set the FileContents for this filter.
182      *
183      * @param fileContents the FileContents for this filter.
184      */
185     private void setFileContents(FileContents fileContents) {
186         fileContentsReference = new WeakReference<>(fileContents);
187     }
188 
189     /**
190      * Setter to specify check pattern to suppress.
191      *
192      * @param format a {@code String} value
193      * @since 5.0
194      */
195     public final void setCheckFormat(String format) {
196         checkFormat = format;
197     }
198 
199     /**
200      * Setter to define message pattern to suppress.
201      *
202      * @param format a {@code String} value
203      * @since 5.0
204      */
205     public void setMessageFormat(String format) {
206         messageFormat = format;
207     }
208 
209     /**
210      * Setter to specify check ID pattern to suppress.
211      *
212      * @param format a {@code String} value
213      * @since 8.24
214      */
215     public void setIdFormat(String format) {
216         idFormat = format;
217     }
218 
219     /**
220      * Setter to specify negative/zero/positive value that defines the number
221      * of lines preceding/at/following the suppression comment.
222      *
223      * @param format a {@code String} value
224      * @since 5.0
225      */
226     public final void setInfluenceFormat(String format) {
227         influenceFormat = format;
228     }
229 
230     /**
231      * Setter to control whether to check C++ style comments ({@code //}).
232      *
233      * @param checkCpp {@code true} if C++ comments are checked.
234      * @since 5.0
235      */
236     // -@cs[AbbreviationAsWordInName] We can not change it as,
237     // check's property is a part of API (used in configurations).
238     public void setCheckCPP(boolean checkCpp) {
239         checkCPP = checkCpp;
240     }
241 
242     /**
243      * Setter to control whether to check C style comments ({@code &#47;* ... *&#47;}).
244      *
245      * @param checkC {@code true} if C comments are checked.
246      * @since 5.0
247      */
248     public void setCheckC(boolean checkC) {
249         this.checkC = checkC;
250     }
251 
252     @Override
253     protected void finishLocalSetup() {
254         // No code by default
255     }
256 
257     @Override
258     public boolean accept(TreeWalkerAuditEvent event) {
259         boolean accepted = true;
260 
261         if (event.getViolation() != null) {
262             // Lazy update. If the first event for the current file, update file
263             // contents and tag suppressions
264             final FileContents currentContents = event.getFileContents();
265 
266             if (getFileContents() != currentContents) {
267                 setFileContents(currentContents);
268                 tagSuppressions();
269             }
270             if (matchesTag(event)) {
271                 accepted = false;
272             }
273         }
274         return accepted;
275     }
276 
277     /**
278      * Whether current event matches any tag from {@link #tags}.
279      *
280      * @param event TreeWalkerAuditEvent to test match on {@link #tags}.
281      * @return true if event matches any tag from {@link #tags}, false otherwise.
282      */
283     private boolean matchesTag(TreeWalkerAuditEvent event) {
284         boolean result = false;
285         for (final Tag tag : tags) {
286             if (tag.isMatch(event)) {
287                 result = true;
288                 break;
289             }
290         }
291         return result;
292     }
293 
294     /**
295      * Collects all the suppression tags for all comments into a list and
296      * sorts the list.
297      */
298     private void tagSuppressions() {
299         tags.clear();
300         final FileContents contents = getFileContents();
301         if (checkCPP) {
302             tagSuppressions(contents.getSingleLineComments().values());
303         }
304         if (checkC) {
305             final Collection<List<TextBlock>> cComments =
306                 contents.getBlockComments().values();
307             cComments.forEach(this::tagSuppressions);
308         }
309     }
310 
311     /**
312      * Appends the suppressions in a collection of comments to the full
313      * set of suppression tags.
314      *
315      * @param comments the set of comments.
316      */
317     private void tagSuppressions(Collection<TextBlock> comments) {
318         for (final TextBlock comment : comments) {
319             final int startLineNo = comment.getStartLineNo();
320             final String[] text = comment.getText();
321             tagCommentLine(text[0], startLineNo);
322             for (int i = 1; i < text.length; i++) {
323                 tagCommentLine(text[i], startLineNo + i);
324             }
325         }
326     }
327 
328     /**
329      * Tags a string if it matches the format for turning
330      * checkstyle reporting on or the format for turning reporting off.
331      *
332      * @param text the string to tag.
333      * @param line the line number of text.
334      */
335     private void tagCommentLine(String text, int line) {
336         final Matcher matcher = commentFormat.matcher(text);
337         if (matcher.find()) {
338             addTag(matcher.group(0), line);
339         }
340     }
341 
342     /**
343      * Adds a comment suppression {@code Tag} to the list of all tags.
344      *
345      * @param text the text of the tag.
346      * @param line the line number of the tag.
347      */
348     private void addTag(String text, int line) {
349         final Tag tag = new Tag(text, line, this);
350         tags.add(tag);
351     }
352 
353     /**
354      * A Tag holds a suppression comment and its location.
355      */
356     private static final class Tag {
357 
358         /** The text of the tag. */
359         private final String text;
360 
361         /** The first line where warnings may be suppressed. */
362         private final int firstLine;
363 
364         /** The last line where warnings may be suppressed. */
365         private final int lastLine;
366 
367         /** The parsed check regexp, expanded for the text of this tag. */
368         private final Pattern tagCheckRegexp;
369 
370         /** The parsed message regexp, expanded for the text of this tag. */
371         private final Pattern tagMessageRegexp;
372 
373         /** The parsed check ID regexp, expanded for the text of this tag. */
374         private final Pattern tagIdRegexp;
375 
376         /**
377          * Constructs a tag.
378          *
379          * @param text the text of the suppression.
380          * @param line the line number.
381          * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
382          * @throws IllegalArgumentException if unable to parse expanded text.
383          */
384         private Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
385             this.text = text;
386 
387             // Expand regexp for check and message
388             // Does not intern Patterns with Utils.getPattern()
389             String format = "";
390             try {
391                 format = CommonUtil.fillTemplateWithStringsByRegexp(
392                         filter.checkFormat, text, filter.commentFormat);
393                 tagCheckRegexp = Pattern.compile(format);
394                 if (filter.messageFormat == null) {
395                     tagMessageRegexp = null;
396                 }
397                 else {
398                     format = CommonUtil.fillTemplateWithStringsByRegexp(
399                             filter.messageFormat, text, filter.commentFormat);
400                     tagMessageRegexp = Pattern.compile(format);
401                 }
402                 if (filter.idFormat == null) {
403                     tagIdRegexp = null;
404                 }
405                 else {
406                     format = CommonUtil.fillTemplateWithStringsByRegexp(
407                             filter.idFormat, text, filter.commentFormat);
408                     tagIdRegexp = Pattern.compile(format);
409                 }
410                 format = CommonUtil.fillTemplateWithStringsByRegexp(
411                         filter.influenceFormat, text, filter.commentFormat);
412 
413                 final int influence = parseInfluence(format, filter.influenceFormat, text);
414 
415                 if (influence >= 1) {
416                     firstLine = line;
417                     lastLine = line + influence;
418                 }
419                 else {
420                     firstLine = line + influence;
421                     lastLine = line;
422                 }
423             }
424             catch (final PatternSyntaxException ex) {
425                 throw new IllegalArgumentException(
426                     "unable to parse expanded comment " + format, ex);
427             }
428         }
429 
430         /**
431          * Gets influence from suppress filter influence format param.
432          *
433          * @param format          influence format to parse
434          * @param influenceFormat raw influence format
435          * @param text            text of the suppression
436          * @return parsed influence
437          * @throws IllegalArgumentException when unable to parse int in format
438          */
439         private static int parseInfluence(String format, String influenceFormat, String text) {
440             try {
441                 return Integer.parseInt(format);
442             }
443             catch (final NumberFormatException ex) {
444                 throw new IllegalArgumentException("unable to parse influence from '" + text
445                         + "' using " + influenceFormat, ex);
446             }
447         }
448 
449         @Override
450         public boolean equals(Object other) {
451             if (this == other) {
452                 return true;
453             }
454             if (other == null || getClass() != other.getClass()) {
455                 return false;
456             }
457             final Tag tag = (Tag) other;
458             return Objects.equals(firstLine, tag.firstLine)
459                     && Objects.equals(lastLine, tag.lastLine)
460                     && Objects.equals(text, tag.text)
461                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
462                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
463                     && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
464         }
465 
466         @Override
467         public int hashCode() {
468             return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp,
469                     tagIdRegexp);
470         }
471 
472         /**
473          * Determines whether the source of an audit event
474          * matches the text of this tag.
475          *
476          * @param event the {@code TreeWalkerAuditEvent} to check.
477          * @return true if the source of event matches the text of this tag.
478          */
479         public boolean isMatch(TreeWalkerAuditEvent event) {
480             return isInScopeOfSuppression(event)
481                     && isCheckMatch(event)
482                     && isIdMatch(event)
483                     && isMessageMatch(event);
484         }
485 
486         /**
487          * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
488          *
489          * @param event {@link TreeWalkerAuditEvent} instance.
490          * @return true if the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
491          */
492         private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) {
493             final int line = event.getLine();
494             return line >= firstLine && line <= lastLine;
495         }
496 
497         /**
498          * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
499          *
500          * @param event {@link TreeWalkerAuditEvent} instance.
501          * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
502          */
503         private boolean isCheckMatch(TreeWalkerAuditEvent event) {
504             final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
505             return checkMatcher.find();
506         }
507 
508         /**
509          * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
510          *
511          * @param event {@link TreeWalkerAuditEvent} instance.
512          * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
513          */
514         private boolean isIdMatch(TreeWalkerAuditEvent event) {
515             boolean match = true;
516             if (tagIdRegexp != null) {
517                 if (event.getModuleId() == null) {
518                     match = false;
519                 }
520                 else {
521                     final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
522                     match = idMatcher.find();
523                 }
524             }
525             return match;
526         }
527 
528         /**
529          * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
530          *
531          * @param event {@link TreeWalkerAuditEvent} instance.
532          * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
533          */
534         private boolean isMessageMatch(TreeWalkerAuditEvent event) {
535             boolean match = true;
536             if (tagMessageRegexp != null) {
537                 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
538                 match = messageMatcher.find();
539             }
540             return match;
541         }
542 
543         @Override
544         public String toString() {
545             return "Tag[text='" + text + '\''
546                     + ", firstLine=" + firstLine
547                     + ", lastLine=" + lastLine
548                     + ", tagCheckRegexp=" + tagCheckRegexp
549                     + ", tagMessageRegexp=" + tagMessageRegexp
550                     + ", tagIdRegexp=" + tagIdRegexp
551                     + ']';
552         }
553 
554     }
555 
556 }