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