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