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