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.Comparator;
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  import com.puppycrawl.tools.checkstyle.utils.WeakReferenceHolder;
40  
41  /**
42   * <div>
43   * Filter {@code SuppressionCommentFilter} uses pairs of comments to suppress audit events.
44   * </div>
45   *
46   * <p>
47   * Rationale:
48   * Sometimes there are legitimate reasons for violating a check. When
49   * this is a matter of the code in question and not personal
50   * preference, the best place to override the policy is in the code
51   * itself. Semi-structured comments can be associated with the check.
52   * This is sometimes superior to a separate suppressions file, which
53   * must be kept up-to-date as the source file is edited.
54   * </p>
55   *
56   * <p>
57   * Note that the suppression comment should be put before the violation.
58   * You can use more than one suppression comment each on separate line.
59   * </p>
60   *
61   * <p>
62   * Attention: This filter may only be specified within the TreeWalker module
63   * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also
64   * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, a
65   * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html">
66   * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
67   * </p>
68   *
69   * <p>
70   * Notes:
71   * {@code offCommentFormat} and {@code onCommentFormat} must have equal
72   * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
73   * paren counts</a>.
74   * </p>
75   *
76   * <p>
77   * SuppressionCommentFilter can suppress Checks that have Treewalker as parent module.
78   * </p>
79   *
80   * @since 3.5
81   */
82  public class SuppressionCommentFilter
83      extends AbstractAutomaticBean
84      implements TreeWalkerFilter {
85  
86      /**
87       * Enum to be used for switching checkstyle reporting for tags.
88       */
89      public enum TagType {
90  
91          /**
92           * Switch reporting on.
93           */
94          ON,
95          /**
96           * Switch reporting off.
97           */
98          OFF,
99  
100     }
101 
102     /** Turns checkstyle reporting off. */
103     private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
104 
105     /** Turns checkstyle reporting on. */
106     private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
107 
108     /** Control all checks. */
109     private static final String DEFAULT_CHECK_FORMAT = ".*";
110 
111     /** Tagged comments. */
112     private final List<Tag> tags = new ArrayList<>();
113 
114     /**
115      * References the current FileContents for this filter.
116      * Since this is a weak reference to the FileContents, the FileContents
117      * can be reclaimed as soon as the strong references in TreeWalker
118      * are reassigned to the next FileContents, at which time filtering for
119      * the current FileContents is finished.
120      */
121     private final WeakReferenceHolder<FileContents> fileContentsHolder =
122             new WeakReferenceHolder<>();
123 
124     /** Control whether to check C style comments (&#47;* ... *&#47;). */
125     private boolean checkC = true;
126 
127     /** Control whether to check C++ style comments ({@code //}). */
128     // -@cs[AbbreviationAsWordInName] we can not change it as,
129     // Check property is a part of API (used in configurations)
130     private boolean checkCPP = true;
131 
132     /** Specify comment pattern to trigger filter to begin suppression. */
133     private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
134 
135     /** Specify comment pattern to trigger filter to end suppression. */
136     private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
137 
138     /** Specify check pattern to suppress. */
139     @XdocsPropertyType(PropertyType.PATTERN)
140     private String checkFormat = DEFAULT_CHECK_FORMAT;
141 
142     /** Specify 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      * Creates a new {@code SuppressionCommentFilter} instance.
152      */
153     public SuppressionCommentFilter() {
154         // no code by default
155     }
156 
157     /**
158      * Setter to specify comment pattern to trigger filter to begin suppression.
159      *
160      * @param pattern a pattern.
161      * @since 3.5
162      */
163     public final void setOffCommentFormat(Pattern pattern) {
164         offCommentFormat = pattern;
165     }
166 
167     /**
168      * Setter to specify comment pattern to trigger filter to end suppression.
169      *
170      * @param pattern a pattern.
171      * @since 3.5
172      */
173     public final void setOnCommentFormat(Pattern pattern) {
174         onCommentFormat = pattern;
175     }
176 
177     /**
178      * Setter to specify check pattern to suppress.
179      * The pattern is matched against the fully qualified class name of the Check.
180      *
181      * @param format a {@code String} value
182      * @since 3.5
183      */
184     public final void setCheckFormat(String format) {
185         checkFormat = format;
186     }
187 
188     /**
189      * Setter to specify message pattern to suppress.
190      *
191      * @param format a {@code String} value
192      * @since 3.5
193      */
194     public void setMessageFormat(String format) {
195         messageFormat = format;
196     }
197 
198     /**
199      * Setter to specify check ID pattern to suppress.
200      *
201      * @param format a {@code String} value
202      * @since 8.24
203      */
204     public void setIdFormat(String format) {
205         idFormat = format;
206     }
207 
208     /**
209      * Setter to control whether to check C++ style comments ({@code //}).
210      *
211      * @param checkCppComments {@code true} if C++ comments are checked.
212      * @since 3.5
213      */
214     // -@cs[AbbreviationAsWordInName] We can not change it as,
215     // check's property is a part of API (used in configurations).
216     public void setCheckCPP(boolean checkCppComments) {
217         checkCPP = checkCppComments;
218     }
219 
220     /**
221      * Setter to control whether to check C style comments (&#47;* ... *&#47;).
222      *
223      * @param checkC {@code true} if C comments are checked.
224      * @since 3.5
225      */
226     public void setCheckC(boolean checkC) {
227         this.checkC = checkC;
228     }
229 
230     @Override
231     protected void finishLocalSetup() {
232         // No code by default
233     }
234 
235     @Override
236     public boolean accept(TreeWalkerAuditEvent event) {
237         boolean accepted = true;
238 
239         if (event.violation() != null) {
240             // Lazy update. If the first event for the current file, update file
241             // contents and tag suppressions
242             final FileContents currentContents = event.fileContents();
243             fileContentsHolder.lazyUpdate(currentContents, this::tagSuppressions);
244             final Tag matchTag = findNearestMatch(event);
245             accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
246         }
247         return accepted;
248     }
249 
250     /**
251      * Finds the nearest comment text tag that matches an audit event.
252      * The nearest tag is before the line and column of the event.
253      *
254      * @param event the {@code TreeWalkerAuditEvent} to match.
255      * @return The {@code Tag} nearest event.
256      */
257     private Tag findNearestMatch(TreeWalkerAuditEvent event) {
258         Tag result = null;
259         for (Tag tag : tags) {
260             final int eventLine = event.getLine();
261             if (tag.getLine() > eventLine
262                 || tag.getLine() == eventLine
263                     && tag.getColumn() > event.getColumn()) {
264                 break;
265             }
266             if (tag.isMatch(event)) {
267                 result = tag;
268             }
269         }
270         return result;
271     }
272 
273     /**
274      * Collects all the suppression tags for all comments into a list and
275      * sorts the list.
276      */
277     private void tagSuppressions() {
278         tags.clear();
279         final FileContents contents = fileContentsHolder.get();
280         if (checkCPP) {
281             tagSuppressions(contents.getSingleLineComments().values());
282         }
283         if (checkC) {
284             final Collection<List<TextBlock>> cComments = contents
285                     .getBlockComments().values();
286             cComments.forEach(this::tagSuppressions);
287         }
288         tags.sort(Comparator.naturalOrder());
289     }
290 
291     /**
292      * Appends the suppressions in a collection of comments to the full
293      * set of suppression tags.
294      *
295      * @param comments the set of comments.
296      */
297     private void tagSuppressions(Collection<TextBlock> comments) {
298         for (TextBlock comment : comments) {
299             final int startLineNo = comment.getStartLineNo();
300             final String[] text = comment.getText();
301             tagCommentLine(text[0], startLineNo, comment.getStartColNo());
302             for (int i = 1; i < text.length; i++) {
303                 tagCommentLine(text[i], startLineNo + i, 0);
304             }
305         }
306     }
307 
308     /**
309      * Tags a string if it matches the format for turning
310      * checkstyle reporting on or the format for turning reporting off.
311      *
312      * @param text the string to tag.
313      * @param line the line number of text.
314      * @param column the column number of text.
315      */
316     private void tagCommentLine(String text, int line, int column) {
317         final Matcher offMatcher = offCommentFormat.matcher(text);
318         if (offMatcher.find()) {
319             addTag(offMatcher.group(0), line, column, TagType.OFF);
320         }
321         else {
322             final Matcher onMatcher = onCommentFormat.matcher(text);
323             if (onMatcher.find()) {
324                 addTag(onMatcher.group(0), line, column, TagType.ON);
325             }
326         }
327     }
328 
329     /**
330      * Adds a {@code Tag} to the list of all tags.
331      *
332      * @param text the text of the tag.
333      * @param line the line number of the tag.
334      * @param column the column number of the tag.
335      * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
336      */
337     private void addTag(String text, int line, int column, TagType reportingOn) {
338         final Tag tag = new Tag(line, column, text, reportingOn, this);
339         tags.add(tag);
340     }
341 
342     /**
343      * A Tag holds a suppression comment and its location, and determines
344      * whether the suppression turns checkstyle reporting on or off.
345      */
346     private static final class Tag
347         implements Comparable<Tag> {
348 
349         /** The text of the tag. */
350         private final String text;
351 
352         /** The line number of the tag. */
353         private final int line;
354 
355         /** The column number of the tag. */
356         private final int column;
357 
358         /** Determines whether the suppression turns checkstyle reporting on. */
359         private final TagType tagType;
360 
361         /** The parsed check regexp, expanded for the text of this tag. */
362         private final Pattern tagCheckRegexp;
363 
364         /** The parsed message regexp, expanded for the text of this tag. */
365         private final Pattern tagMessageRegexp;
366 
367         /** The parsed check ID regexp, expanded for the text of this tag. */
368         private final Pattern tagIdRegexp;
369 
370         /**
371          * Constructs a tag.
372          *
373          * @param line the line number.
374          * @param column the column number.
375          * @param text the text of the suppression.
376          * @param tagType {@code ON} if the tag turns checkstyle reporting.
377          * @param filter the {@code SuppressionCommentFilter} with the context
378          * @throws IllegalArgumentException if unable to parse expanded text.
379          */
380         private Tag(int line, int column, String text, TagType tagType,
381                    SuppressionCommentFilter filter) {
382             this.line = line;
383             this.column = column;
384             this.text = text;
385             this.tagType = tagType;
386 
387             final Pattern commentFormat;
388             if (this.tagType == TagType.ON) {
389                 commentFormat = filter.onCommentFormat;
390             }
391             else {
392                 commentFormat = filter.offCommentFormat;
393             }
394 
395             // Expand regexp for check and message
396             // Does not intern Patterns with Utils.getPattern()
397             String format = "";
398             try {
399                 format = CommonUtil.fillTemplateWithStringsByRegexp(
400                         filter.checkFormat, text, commentFormat);
401                 tagCheckRegexp = Pattern.compile(format);
402 
403                 if (filter.messageFormat == null) {
404                     tagMessageRegexp = null;
405                 }
406                 else {
407                     format = CommonUtil.fillTemplateWithStringsByRegexp(
408                             filter.messageFormat, text, commentFormat);
409                     tagMessageRegexp = Pattern.compile(format);
410                 }
411 
412                 if (filter.idFormat == null) {
413                     tagIdRegexp = null;
414                 }
415                 else {
416                     format = CommonUtil.fillTemplateWithStringsByRegexp(
417                             filter.idFormat, text, commentFormat);
418                     tagIdRegexp = Pattern.compile(format);
419                 }
420             }
421             catch (final PatternSyntaxException exc) {
422                 throw new IllegalArgumentException(
423                     "unable to parse expanded comment " + format, exc);
424             }
425         }
426 
427         /**
428          * Returns line number of the tag in the source file.
429          *
430          * @return the line number of the tag in the source file.
431          */
432         /* package */ int getLine() {
433             return line;
434         }
435 
436         /**
437          * Determines the column number of the tag in the source file.
438          * Will be 0 for all lines of multiline comment, except the
439          * first line.
440          *
441          * @return the column number of the tag in the source file.
442          */
443         /* package */ int getColumn() {
444             return column;
445         }
446 
447         /**
448          * Determines whether the suppression turns checkstyle reporting on or
449          * off.
450          *
451          * @return {@code ON} if the suppression turns reporting on.
452          */
453         /* package */ TagType getTagType() {
454             return tagType;
455         }
456 
457         /**
458          * Compares the position of this tag in the file
459          * with the position of another tag.
460          *
461          * @param object the tag to compare with this one.
462          * @return a negative number if this tag is before the other tag,
463          *     0 if they are at the same position, and a positive number if this
464          *     tag is after the other tag.
465          */
466         @Override
467         public int compareTo(Tag object) {
468             final int result;
469             if (line == object.line) {
470                 result = Integer.compare(column, object.column);
471             }
472             else {
473                 result = Integer.compare(line, object.line);
474             }
475             return result;
476         }
477 
478         /**
479          * Indicates whether some other object is "equal to" this one.
480          * Suppression on enumeration is needed so code stays consistent.
481          *
482          * @noinspection EqualsCalledOnEnumConstant
483          * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
484          *      code consistent
485          */
486         @Override
487         public boolean equals(Object other) {
488             if (this == other) {
489                 return true;
490             }
491             if (other == null || getClass() != other.getClass()) {
492                 return false;
493             }
494             final Tag tag = (Tag) other;
495             return line == tag.line
496                     && column == tag.column
497                     && Objects.equals(tagType, tag.tagType)
498                     && Objects.equals(text, tag.text)
499                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
500                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
501                     && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
502         }
503 
504         @Override
505         public int hashCode() {
506             return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
507                     tagIdRegexp);
508         }
509 
510         /**
511          * Determines whether the source of an audit event
512          * matches the text of this tag.
513          *
514          * @param event the {@code TreeWalkerAuditEvent} to check.
515          * @return true if the source of event matches the text of this tag.
516          */
517         /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
518             return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
519         }
520 
521         /**
522          * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
523          *
524          * @param event {@code TreeWalkerAuditEvent} instance.
525          * @return true if the {@code TreeWalkerAuditEvent} source name matches the check format.
526          */
527         private boolean isCheckMatch(TreeWalkerAuditEvent event) {
528             final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
529             return checkMatcher.find();
530         }
531 
532         /**
533          * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
534          *
535          * @param event {@code TreeWalkerAuditEvent} instance.
536          * @return true if the {@code TreeWalkerAuditEvent} module ID matches the ID format.
537          */
538         private boolean isIdMatch(TreeWalkerAuditEvent event) {
539             boolean match = true;
540             if (tagIdRegexp != null) {
541                 if (event.getModuleId() == null) {
542                     match = false;
543                 }
544                 else {
545                     final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
546                     match = idMatcher.find();
547                 }
548             }
549             return match;
550         }
551 
552         /**
553          * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
554          *
555          * @param event {@code TreeWalkerAuditEvent} instance.
556          * @return true if the {@code TreeWalkerAuditEvent} message matches the message format.
557          */
558         private boolean isMessageMatch(TreeWalkerAuditEvent event) {
559             boolean match = true;
560             if (tagMessageRegexp != null) {
561                 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
562                 match = messageMatcher.find();
563             }
564             return match;
565         }
566 
567         @Override
568         public String toString() {
569             return "Tag[text='" + text + '\''
570                     + ", line=" + line
571                     + ", column=" + column
572                     + ", type=" + tagType
573                     + ", tagCheckRegexp=" + tagCheckRegexp
574                     + ", tagMessageRegexp=" + tagMessageRegexp
575                     + ", tagIdRegexp=" + tagIdRegexp + ']';
576         }
577 
578     }
579 
580 }