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