View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.filters;
21  
22  import java.io.IOException;
23  import java.nio.charset.StandardCharsets;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Objects;
29  import java.util.Optional;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  import java.util.regex.PatternSyntaxException;
33  
34  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
35  import com.puppycrawl.tools.checkstyle.PropertyType;
36  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
37  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
38  import com.puppycrawl.tools.checkstyle.api.FileText;
39  import com.puppycrawl.tools.checkstyle.api.Filter;
40  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
41  
42  /**
43   * <div>
44   * Filter {@code SuppressWithPlainTextCommentFilter} uses plain text to suppress
45   * audit events. The filter knows nothing about AST, it treats only plain text
46   * comments and extracts the information required for suppression from the plain
47   * text comments. Currently, the filter supports only single-line comments.
48   * </div>
49   *
50   * <p>
51   * Please, be aware of the fact that, it is not recommended to use the filter
52   * for Java code anymore.
53   * </p>
54   *
55   * <p>
56   * Rationale: Sometimes there are legitimate reasons for violating a check.
57   * When this is a matter of the code in question and not personal preference,
58   * the best place to override the policy is in the code itself. Semi-structured
59   * comments can be associated with the check. This is sometimes superior to
60   * a separate suppressions file, which must be kept up-to-date as the source
61   * file is edited.
62   * </p>
63   *
64   * <p>
65   * Note that the suppression comment should be put before the violation.
66   * You can use more than one suppression comment each on separate line.
67   * </p>
68   *
69   * <p>
70   * Notes:
71   * Properties {@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   * SuppressWithPlainTextCommentFilter can suppress Checks that have Treewalker or
78   * Checker as parent module.
79   * </p>
80   *
81   * @since 8.6
82   */
83  public class SuppressWithPlainTextCommentFilter extends AbstractAutomaticBean implements Filter {
84  
85      /** Comment format which turns checkstyle reporting off. */
86      private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
87  
88      /** Comment format which turns checkstyle reporting on. */
89      private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
90  
91      /** Default check format to suppress. By default, the filter suppress all checks. */
92      private static final String DEFAULT_CHECK_FORMAT = ".*";
93  
94      /** List of suppressions from the file. By default, Its null. */
95      private final Collection<Suppression> currentFileSuppressionCache = new ArrayList<>();
96  
97      /** File name that was suppressed. By default, Its empty. */
98      private String currentFileName = "";
99  
100     /** Specify comment pattern to trigger filter to begin suppression. */
101     private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT);
102 
103     /** Specify comment pattern to trigger filter to end suppression. */
104     private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT);
105 
106     /** Specify check pattern to suppress. */
107     @XdocsPropertyType(PropertyType.PATTERN)
108     private String checkFormat = DEFAULT_CHECK_FORMAT;
109 
110     /** Specify message pattern to suppress. */
111     @XdocsPropertyType(PropertyType.PATTERN)
112     private String messageFormat;
113 
114     /** Specify check ID pattern to suppress. */
115     @XdocsPropertyType(PropertyType.PATTERN)
116     private String idFormat;
117 
118     /**
119      * Creates a new {@code SuppressWithPlainTextCommentFilter} instance.
120      */
121     public SuppressWithPlainTextCommentFilter() {
122         // no code by default
123     }
124 
125     /**
126      * Setter to specify comment pattern to trigger filter to begin suppression.
127      *
128      * @param pattern off comment format pattern.
129      * @since 8.6
130      */
131     public final void setOffCommentFormat(Pattern pattern) {
132         offCommentFormat = pattern;
133     }
134 
135     /**
136      * Setter to specify comment pattern to trigger filter to end suppression.
137      *
138      * @param pattern  on comment format pattern.
139      * @since 8.6
140      */
141     public final void setOnCommentFormat(Pattern pattern) {
142         onCommentFormat = pattern;
143     }
144 
145     /**
146      * Setter to specify check pattern to suppress.
147      * The pattern is matched against the fully qualified class name of the Check.
148      *
149      * @param format pattern for check format.
150      * @since 8.6
151      */
152     public final void setCheckFormat(String format) {
153         checkFormat = format;
154     }
155 
156     /**
157      * Setter to specify message pattern to suppress.
158      *
159      * @param format pattern for message format.
160      * @since 8.6
161      */
162     public final void setMessageFormat(String format) {
163         messageFormat = format;
164     }
165 
166     /**
167      * Setter to specify check ID pattern to suppress.
168      *
169      * @param format pattern for check ID format
170      * @since 8.24
171      */
172     public final void setIdFormat(String format) {
173         idFormat = format;
174     }
175 
176     @Override
177     public boolean accept(AuditEvent event) {
178         boolean accepted = true;
179         if (event.getViolation() != null) {
180             final String eventFileName = event.getFileName();
181 
182             if (!currentFileName.equals(eventFileName)) {
183                 currentFileName = eventFileName;
184                 final FileText fileText = getFileText(eventFileName);
185                 currentFileSuppressionCache.clear();
186                 if (fileText != null) {
187                     cacheSuppressions(fileText);
188                 }
189             }
190 
191             accepted = getNearestSuppression(currentFileSuppressionCache, event) == null;
192         }
193         return accepted;
194     }
195 
196     @Override
197     protected void finishLocalSetup() {
198         // No code by default
199     }
200 
201     /**
202      * Caches {@link FileText} instance created based on the given file name.
203      *
204      * @param fileName the name of the file.
205      * @return {@code FileText} instance.
206      * @throws IllegalStateException if the file could not be read.
207      */
208     private static FileText getFileText(String fileName) {
209         final Path path = Path.of(fileName);
210         FileText result = null;
211 
212         // some violations can be on a directory, instead of a file
213         if (!Files.isDirectory(path)) {
214             try {
215                 result = new FileText(path.toFile(), StandardCharsets.UTF_8.name());
216             }
217             catch (IOException exc) {
218                 throw new IllegalStateException("Cannot read source file: " + fileName, exc);
219             }
220         }
221 
222         return result;
223     }
224 
225     /**
226      * Collects the list of {@link Suppression} instances retrieved from the given {@link FileText}.
227      *
228      * @param fileText {@code FileText} instance.
229      */
230     private void cacheSuppressions(FileText fileText) {
231         for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
232             final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
233             suppression.ifPresent(currentFileSuppressionCache::add);
234         }
235     }
236 
237     /**
238      * Tries to extract the suppression from the given line.
239      *
240      * @param fileText {@link FileText} instance.
241      * @param lineNo line number.
242      * @return {@link Optional} of {@link Suppression}.
243      */
244     private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
245         final String line = fileText.get(lineNo);
246         final Matcher onCommentMatcher = onCommentFormat.matcher(line);
247         final Matcher offCommentMatcher = offCommentFormat.matcher(line);
248 
249         Suppression suppression = null;
250         if (onCommentMatcher.find()) {
251             suppression = new Suppression(onCommentMatcher.group(0),
252                 lineNo + 1, SuppressionType.ON, this);
253         }
254         if (offCommentMatcher.find()) {
255             suppression = new Suppression(offCommentMatcher.group(0),
256                 lineNo + 1, SuppressionType.OFF, this);
257         }
258 
259         return Optional.ofNullable(suppression);
260     }
261 
262     /**
263      * Finds the nearest {@link Suppression} instance which can suppress
264      * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
265      * is before the line and column of the event.
266      *
267      * @param suppressions collection of {@code Suppression} instances.
268      * @param event {@code AuditEvent} instance.
269      * @return {@code Suppression} instance.
270      */
271     private static Suppression getNearestSuppression(Collection<Suppression> suppressions,
272                                                      AuditEvent event) {
273         return suppressions
274             .stream()
275             .filter(suppression -> suppression.isMatch(event))
276             .reduce((first, second) -> second)
277             .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
278             .orElse(null);
279     }
280 
281     /** Enum which represents the type of the suppression. */
282     private enum SuppressionType {
283 
284         /** On suppression type. */
285         ON,
286         /** Off suppression type. */
287         OFF,
288 
289     }
290 
291     /** The class which represents the suppression. */
292     private static final class Suppression {
293 
294         /** The regexp which is used to match the event source.*/
295         private final Pattern eventSourceRegexp;
296         /** The regexp which is used to match the event message.*/
297         private final Pattern eventMessageRegexp;
298         /** The regexp which is used to match the event ID.*/
299         private final Pattern eventIdRegexp;
300 
301         /** Suppression line.*/
302         private final int lineNo;
303 
304         /** Suppression type. */
305         private final SuppressionType suppressionType;
306 
307         /**
308          * Creates new suppression instance.
309          *
310          * @param text suppression text.
311          * @param lineNo suppression line number.
312          * @param suppressionType suppression type.
313          * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
314          * @throws IllegalArgumentException if there is an error in the filter regex syntax.
315          */
316         private Suppression(
317             String text,
318             int lineNo,
319             SuppressionType suppressionType,
320             SuppressWithPlainTextCommentFilter filter
321         ) {
322             this.lineNo = lineNo;
323             this.suppressionType = suppressionType;
324 
325             final Pattern commentFormat;
326             if (this.suppressionType == SuppressionType.ON) {
327                 commentFormat = filter.onCommentFormat;
328             }
329             else {
330                 commentFormat = filter.offCommentFormat;
331             }
332 
333             // Expand regexp for check and message
334             // Does not intern Patterns with Utils.getPattern()
335             String format = "";
336             try {
337                 format = CommonUtil.fillTemplateWithStringsByRegexp(
338                         filter.checkFormat, text, commentFormat);
339                 eventSourceRegexp = Pattern.compile(format);
340                 if (filter.messageFormat == null) {
341                     eventMessageRegexp = null;
342                 }
343                 else {
344                     format = CommonUtil.fillTemplateWithStringsByRegexp(
345                             filter.messageFormat, text, commentFormat);
346                     eventMessageRegexp = Pattern.compile(format);
347                 }
348                 if (filter.idFormat == null) {
349                     eventIdRegexp = null;
350                 }
351                 else {
352                     format = CommonUtil.fillTemplateWithStringsByRegexp(
353                             filter.idFormat, text, commentFormat);
354                     eventIdRegexp = Pattern.compile(format);
355                 }
356             }
357             catch (final PatternSyntaxException exc) {
358                 throw new IllegalArgumentException(
359                     "unable to parse expanded comment " + format, exc);
360             }
361         }
362 
363         /**
364          * Indicates whether some other object is "equal to" this one.
365          *
366          * @noinspection EqualsCalledOnEnumConstant
367          * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
368          *      code consistent
369          */
370         @Override
371         public boolean equals(Object other) {
372             if (this == other) {
373                 return true;
374             }
375             if (other == null || getClass() != other.getClass()) {
376                 return false;
377             }
378             final Suppression suppression = (Suppression) other;
379             return lineNo == suppression.lineNo
380                     && Objects.equals(suppressionType, suppression.suppressionType)
381                     && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
382                     && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp)
383                     && Objects.equals(eventIdRegexp, suppression.eventIdRegexp);
384         }
385 
386         @Override
387         public int hashCode() {
388             return Objects.hash(
389                 lineNo, suppressionType, eventSourceRegexp, eventMessageRegexp,
390                 eventIdRegexp);
391         }
392 
393         /**
394          * Checks whether the suppression matches the given {@link AuditEvent}.
395          *
396          * @param event {@code AuditEvent} instance.
397          * @return true if the suppression matches {@code AuditEvent}.
398          */
399         private boolean isMatch(AuditEvent event) {
400             return isInScopeOfSuppression(event)
401                     && isCheckMatch(event)
402                     && isIdMatch(event)
403                     && isMessageMatch(event);
404         }
405 
406         /**
407          * Checks whether {@link AuditEvent} is in the scope of the suppression.
408          *
409          * @param event {@code AuditEvent} instance.
410          * @return true if {@code AuditEvent} is in the scope of the suppression.
411          */
412         private boolean isInScopeOfSuppression(AuditEvent event) {
413             return lineNo <= event.getLine();
414         }
415 
416         /**
417          * Checks whether {@link AuditEvent} source name matches the check format.
418          *
419          * @param event {@code AuditEvent} instance.
420          * @return true if the {@code AuditEvent} source name matches the check format.
421          */
422         private boolean isCheckMatch(AuditEvent event) {
423             final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
424             return checkMatcher.find();
425         }
426 
427         /**
428          * Checks whether the {@link AuditEvent} module ID matches the ID format.
429          *
430          * @param event {@code AuditEvent} instance.
431          * @return true if the {@code AuditEvent} module ID matches the ID format.
432          */
433         private boolean isIdMatch(AuditEvent event) {
434             boolean match = true;
435             if (eventIdRegexp != null) {
436                 if (event.getModuleId() == null) {
437                     match = false;
438                 }
439                 else {
440                     final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
441                     match = idMatcher.find();
442                 }
443             }
444             return match;
445         }
446 
447         /**
448          * Checks whether the {@link AuditEvent} message matches the message format.
449          *
450          * @param event {@code AuditEvent} instance.
451          * @return true if the {@code AuditEvent} message matches the message format.
452          */
453         private boolean isMessageMatch(AuditEvent event) {
454             boolean match = true;
455             if (eventMessageRegexp != null) {
456                 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
457                 match = messageMatcher.find();
458             }
459             return match;
460         }
461     }
462 
463 }