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