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