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