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