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