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.Collections;
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 &lt;module name="TreeWalker"/&gt;}) 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 ({@code &#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      * Setter to specify comment pattern to trigger filter to begin suppression.
152      *
153      * @param pattern a pattern.
154      * @since 3.5
155      */
156     public final void setOffCommentFormat(Pattern pattern) {
157         offCommentFormat = pattern;
158     }
159 
160     /**
161      * Setter to specify comment pattern to trigger filter to end suppression.
162      *
163      * @param pattern a pattern.
164      * @since 3.5
165      */
166     public final void setOnCommentFormat(Pattern pattern) {
167         onCommentFormat = pattern;
168     }
169 
170     /**
171      * Setter to specify check pattern to suppress.
172      *
173      * @param format a {@code String} value
174      * @since 3.5
175      */
176     public final void setCheckFormat(String format) {
177         checkFormat = format;
178     }
179 
180     /**
181      * Setter to specify message pattern to suppress.
182      *
183      * @param format a {@code String} value
184      * @since 3.5
185      */
186     public void setMessageFormat(String format) {
187         messageFormat = format;
188     }
189 
190     /**
191      * Setter to specify check ID pattern to suppress.
192      *
193      * @param format a {@code String} value
194      * @since 8.24
195      */
196     public void setIdFormat(String format) {
197         idFormat = format;
198     }
199 
200     /**
201      * Setter to control whether to check C++ style comments ({@code //}).
202      *
203      * @param checkCppComments {@code true} if C++ comments are checked.
204      * @since 3.5
205      */
206     // -@cs[AbbreviationAsWordInName] We can not change it as,
207     // check's property is a part of API (used in configurations).
208     public void setCheckCPP(boolean checkCppComments) {
209         checkCPP = checkCppComments;
210     }
211 
212     /**
213      * Setter to control whether to check C style comments ({@code &#47;* ... *&#47;}).
214      *
215      * @param checkC {@code true} if C comments are checked.
216      * @since 3.5
217      */
218     public void setCheckC(boolean checkC) {
219         this.checkC = checkC;
220     }
221 
222     @Override
223     protected void finishLocalSetup() {
224         // No code by default
225     }
226 
227     @Override
228     public boolean accept(TreeWalkerAuditEvent event) {
229         boolean accepted = true;
230 
231         if (event.violation() != null) {
232             // Lazy update. If the first event for the current file, update file
233             // contents and tag suppressions
234             final FileContents currentContents = event.fileContents();
235             fileContentsHolder.lazyUpdate(currentContents, this::tagSuppressions);
236             final Tag matchTag = findNearestMatch(event);
237             accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
238         }
239         return accepted;
240     }
241 
242     /**
243      * Finds the nearest comment text tag that matches an audit event.
244      * The nearest tag is before the line and column of the event.
245      *
246      * @param event the {@code TreeWalkerAuditEvent} to match.
247      * @return The {@code Tag} nearest event.
248      */
249     private Tag findNearestMatch(TreeWalkerAuditEvent event) {
250         Tag result = null;
251         for (Tag tag : tags) {
252             final int eventLine = event.getLine();
253             if (tag.getLine() > eventLine
254                 || tag.getLine() == eventLine
255                     && tag.getColumn() > event.getColumn()) {
256                 break;
257             }
258             if (tag.isMatch(event)) {
259                 result = tag;
260             }
261         }
262         return result;
263     }
264 
265     /**
266      * Collects all the suppression tags for all comments into a list and
267      * sorts the list.
268      */
269     private void tagSuppressions() {
270         tags.clear();
271         final FileContents contents = fileContentsHolder.get();
272         if (checkCPP) {
273             tagSuppressions(contents.getSingleLineComments().values());
274         }
275         if (checkC) {
276             final Collection<List<TextBlock>> cComments = contents
277                     .getBlockComments().values();
278             cComments.forEach(this::tagSuppressions);
279         }
280         Collections.sort(tags);
281     }
282 
283     /**
284      * Appends the suppressions in a collection of comments to the full
285      * set of suppression tags.
286      *
287      * @param comments the set of comments.
288      */
289     private void tagSuppressions(Collection<TextBlock> comments) {
290         for (TextBlock comment : comments) {
291             final int startLineNo = comment.getStartLineNo();
292             final String[] text = comment.getText();
293             tagCommentLine(text[0], startLineNo, comment.getStartColNo());
294             for (int i = 1; i < text.length; i++) {
295                 tagCommentLine(text[i], startLineNo + i, 0);
296             }
297         }
298     }
299 
300     /**
301      * Tags a string if it matches the format for turning
302      * checkstyle reporting on or the format for turning reporting off.
303      *
304      * @param text the string to tag.
305      * @param line the line number of text.
306      * @param column the column number of text.
307      */
308     private void tagCommentLine(String text, int line, int column) {
309         final Matcher offMatcher = offCommentFormat.matcher(text);
310         if (offMatcher.find()) {
311             addTag(offMatcher.group(0), line, column, TagType.OFF);
312         }
313         else {
314             final Matcher onMatcher = onCommentFormat.matcher(text);
315             if (onMatcher.find()) {
316                 addTag(onMatcher.group(0), line, column, TagType.ON);
317             }
318         }
319     }
320 
321     /**
322      * Adds a {@code Tag} to the list of all tags.
323      *
324      * @param text the text of the tag.
325      * @param line the line number of the tag.
326      * @param column the column number of the tag.
327      * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
328      */
329     private void addTag(String text, int line, int column, TagType reportingOn) {
330         final Tag tag = new Tag(line, column, text, reportingOn, this);
331         tags.add(tag);
332     }
333 
334     /**
335      * A Tag holds a suppression comment and its location, and determines
336      * whether the suppression turns checkstyle reporting on or off.
337      */
338     private static final class Tag
339         implements Comparable<Tag> {
340 
341         /** The text of the tag. */
342         private final String text;
343 
344         /** The line number of the tag. */
345         private final int line;
346 
347         /** The column number of the tag. */
348         private final int column;
349 
350         /** Determines whether the suppression turns checkstyle reporting on. */
351         private final TagType tagType;
352 
353         /** The parsed check regexp, expanded for the text of this tag. */
354         private final Pattern tagCheckRegexp;
355 
356         /** The parsed message regexp, expanded for the text of this tag. */
357         private final Pattern tagMessageRegexp;
358 
359         /** The parsed check ID regexp, expanded for the text of this tag. */
360         private final Pattern tagIdRegexp;
361 
362         /**
363          * Constructs a tag.
364          *
365          * @param line the line number.
366          * @param column the column number.
367          * @param text the text of the suppression.
368          * @param tagType {@code ON} if the tag turns checkstyle reporting.
369          * @param filter the {@code SuppressionCommentFilter} with the context
370          * @throws IllegalArgumentException if unable to parse expanded text.
371          */
372         private Tag(int line, int column, String text, TagType tagType,
373                    SuppressionCommentFilter filter) {
374             this.line = line;
375             this.column = column;
376             this.text = text;
377             this.tagType = tagType;
378 
379             final Pattern commentFormat;
380             if (this.tagType == TagType.ON) {
381                 commentFormat = filter.onCommentFormat;
382             }
383             else {
384                 commentFormat = filter.offCommentFormat;
385             }
386 
387             // Expand regexp for check and message
388             // Does not intern Patterns with Utils.getPattern()
389             String format = "";
390             try {
391                 format = CommonUtil.fillTemplateWithStringsByRegexp(
392                         filter.checkFormat, text, commentFormat);
393                 tagCheckRegexp = Pattern.compile(format);
394 
395                 if (filter.messageFormat == null) {
396                     tagMessageRegexp = null;
397                 }
398                 else {
399                     format = CommonUtil.fillTemplateWithStringsByRegexp(
400                             filter.messageFormat, text, commentFormat);
401                     tagMessageRegexp = Pattern.compile(format);
402                 }
403 
404                 if (filter.idFormat == null) {
405                     tagIdRegexp = null;
406                 }
407                 else {
408                     format = CommonUtil.fillTemplateWithStringsByRegexp(
409                             filter.idFormat, text, commentFormat);
410                     tagIdRegexp = Pattern.compile(format);
411                 }
412             }
413             catch (final PatternSyntaxException exc) {
414                 throw new IllegalArgumentException(
415                     "unable to parse expanded comment " + format, exc);
416             }
417         }
418 
419         /**
420          * Returns line number of the tag in the source file.
421          *
422          * @return the line number of the tag in the source file.
423          */
424         /* package */ int getLine() {
425             return line;
426         }
427 
428         /**
429          * Determines the column number of the tag in the source file.
430          * Will be 0 for all lines of multiline comment, except the
431          * first line.
432          *
433          * @return the column number of the tag in the source file.
434          */
435         /* package */ int getColumn() {
436             return column;
437         }
438 
439         /**
440          * Determines whether the suppression turns checkstyle reporting on or
441          * off.
442          *
443          * @return {@code ON} if the suppression turns reporting on.
444          */
445         /* package */ TagType getTagType() {
446             return tagType;
447         }
448 
449         /**
450          * Compares the position of this tag in the file
451          * with the position of another tag.
452          *
453          * @param object the tag to compare with this one.
454          * @return a negative number if this tag is before the other tag,
455          *     0 if they are at the same position, and a positive number if this
456          *     tag is after the other tag.
457          */
458         @Override
459         public int compareTo(Tag object) {
460             final int result;
461             if (line == object.line) {
462                 result = Integer.compare(column, object.column);
463             }
464             else {
465                 result = Integer.compare(line, object.line);
466             }
467             return result;
468         }
469 
470         /**
471          * Indicates whether some other object is "equal to" this one.
472          * Suppression on enumeration is needed so code stays consistent.
473          *
474          * @noinspection EqualsCalledOnEnumConstant
475          * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
476          *      code consistent
477          */
478         @Override
479         public boolean equals(Object other) {
480             if (this == other) {
481                 return true;
482             }
483             if (other == null || getClass() != other.getClass()) {
484                 return false;
485             }
486             final Tag tag = (Tag) other;
487             return Objects.equals(line, tag.line)
488                     && Objects.equals(column, tag.column)
489                     && Objects.equals(tagType, tag.tagType)
490                     && Objects.equals(text, tag.text)
491                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
492                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
493                     && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
494         }
495 
496         @Override
497         public int hashCode() {
498             return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
499                     tagIdRegexp);
500         }
501 
502         /**
503          * Determines whether the source of an audit event
504          * matches the text of this tag.
505          *
506          * @param event the {@code TreeWalkerAuditEvent} to check.
507          * @return true if the source of event matches the text of this tag.
508          */
509         /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
510             return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
511         }
512 
513         /**
514          * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
515          *
516          * @param event {@link TreeWalkerAuditEvent} instance.
517          * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
518          */
519         private boolean isCheckMatch(TreeWalkerAuditEvent event) {
520             final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
521             return checkMatcher.find();
522         }
523 
524         /**
525          * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
526          *
527          * @param event {@link TreeWalkerAuditEvent} instance.
528          * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
529          */
530         private boolean isIdMatch(TreeWalkerAuditEvent event) {
531             boolean match = true;
532             if (tagIdRegexp != null) {
533                 if (event.getModuleId() == null) {
534                     match = false;
535                 }
536                 else {
537                     final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
538                     match = idMatcher.find();
539                 }
540             }
541             return match;
542         }
543 
544         /**
545          * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
546          *
547          * @param event {@link TreeWalkerAuditEvent} instance.
548          * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
549          */
550         private boolean isMessageMatch(TreeWalkerAuditEvent event) {
551             boolean match = true;
552             if (tagMessageRegexp != null) {
553                 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
554                 match = messageMatcher.find();
555             }
556             return match;
557         }
558 
559         @Override
560         public String toString() {
561             return "Tag[text='" + text + '\''
562                     + ", line=" + line
563                     + ", column=" + column
564                     + ", type=" + tagType
565                     + ", tagCheckRegexp=" + tagCheckRegexp
566                     + ", tagMessageRegexp=" + tagMessageRegexp
567                     + ", tagIdRegexp=" + tagIdRegexp + ']';
568         }
569 
570     }
571 
572 }