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      * Setter to specify comment pattern to trigger filter to begin suppression.
120      *
121      * @param pattern off comment format pattern.
122      * @since 8.6
123      */
124     public final void setOffCommentFormat(Pattern pattern) {
125         offCommentFormat = pattern;
126     }
127 
128     /**
129      * Setter to specify comment pattern to trigger filter to end suppression.
130      *
131      * @param pattern  on comment format pattern.
132      * @since 8.6
133      */
134     public final void setOnCommentFormat(Pattern pattern) {
135         onCommentFormat = pattern;
136     }
137 
138     /**
139      * Setter to specify check pattern to suppress.
140      *
141      * @param format pattern for check format.
142      * @since 8.6
143      */
144     public final void setCheckFormat(String format) {
145         checkFormat = format;
146     }
147 
148     /**
149      * Setter to specify message pattern to suppress.
150      *
151      * @param format pattern for message format.
152      * @since 8.6
153      */
154     public final void setMessageFormat(String format) {
155         messageFormat = format;
156     }
157 
158     /**
159      * Setter to specify check ID pattern to suppress.
160      *
161      * @param format pattern for check ID format
162      * @since 8.24
163      */
164     public final void setIdFormat(String format) {
165         idFormat = format;
166     }
167 
168     @Override
169     public boolean accept(AuditEvent event) {
170         boolean accepted = true;
171         if (event.getViolation() != null) {
172             final String eventFileName = event.getFileName();
173 
174             if (!currentFileName.equals(eventFileName)) {
175                 currentFileName = eventFileName;
176                 final FileText fileText = getFileText(eventFileName);
177                 currentFileSuppressionCache.clear();
178                 if (fileText != null) {
179                     cacheSuppressions(fileText);
180                 }
181             }
182 
183             accepted = getNearestSuppression(currentFileSuppressionCache, event) == null;
184         }
185         return accepted;
186     }
187 
188     @Override
189     protected void finishLocalSetup() {
190         // No code by default
191     }
192 
193     /**
194      * Caches {@link FileText} instance created based on the given file name.
195      *
196      * @param fileName the name of the file.
197      * @return {@link FileText} instance.
198      * @throws IllegalStateException if the file could not be read.
199      */
200     private static FileText getFileText(String fileName) {
201         final Path path = Path.of(fileName);
202         FileText result = null;
203 
204         // some violations can be on a directory, instead of a file
205         if (!Files.isDirectory(path)) {
206             try {
207                 result = new FileText(path.toFile(), StandardCharsets.UTF_8.name());
208             }
209             catch (IOException exc) {
210                 throw new IllegalStateException("Cannot read source file: " + fileName, exc);
211             }
212         }
213 
214         return result;
215     }
216 
217     /**
218      * Collects the list of {@link Suppression} instances retrieved from the given {@link FileText}.
219      *
220      * @param fileText {@link FileText} instance.
221      */
222     private void cacheSuppressions(FileText fileText) {
223         for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
224             final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
225             suppression.ifPresent(currentFileSuppressionCache::add);
226         }
227     }
228 
229     /**
230      * Tries to extract the suppression from the given line.
231      *
232      * @param fileText {@link FileText} instance.
233      * @param lineNo line number.
234      * @return {@link Optional} of {@link Suppression}.
235      */
236     private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
237         final String line = fileText.get(lineNo);
238         final Matcher onCommentMatcher = onCommentFormat.matcher(line);
239         final Matcher offCommentMatcher = offCommentFormat.matcher(line);
240 
241         Suppression suppression = null;
242         if (onCommentMatcher.find()) {
243             suppression = new Suppression(onCommentMatcher.group(0),
244                 lineNo + 1, SuppressionType.ON, this);
245         }
246         if (offCommentMatcher.find()) {
247             suppression = new Suppression(offCommentMatcher.group(0),
248                 lineNo + 1, SuppressionType.OFF, this);
249         }
250 
251         return Optional.ofNullable(suppression);
252     }
253 
254     /**
255      * Finds the nearest {@link Suppression} instance which can suppress
256      * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
257      * is before the line and column of the event.
258      *
259      * @param suppressions collection of {@link Suppression} instances.
260      * @param event {@link AuditEvent} instance.
261      * @return {@link Suppression} instance.
262      */
263     private static Suppression getNearestSuppression(Collection<Suppression> suppressions,
264                                                      AuditEvent event) {
265         return suppressions
266             .stream()
267             .filter(suppression -> suppression.isMatch(event))
268             .reduce((first, second) -> second)
269             .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
270             .orElse(null);
271     }
272 
273     /** Enum which represents the type of the suppression. */
274     private enum SuppressionType {
275 
276         /** On suppression type. */
277         ON,
278         /** Off suppression type. */
279         OFF,
280 
281     }
282 
283     /** The class which represents the suppression. */
284     private static final class Suppression {
285 
286         /** The regexp which is used to match the event source.*/
287         private final Pattern eventSourceRegexp;
288         /** The regexp which is used to match the event message.*/
289         private final Pattern eventMessageRegexp;
290         /** The regexp which is used to match the event ID.*/
291         private final Pattern eventIdRegexp;
292 
293         /** Suppression line.*/
294         private final int lineNo;
295 
296         /** Suppression type. */
297         private final SuppressionType suppressionType;
298 
299         /**
300          * Creates new suppression instance.
301          *
302          * @param text suppression text.
303          * @param lineNo suppression line number.
304          * @param suppressionType suppression type.
305          * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
306          * @throws IllegalArgumentException if there is an error in the filter regex syntax.
307          */
308         private Suppression(
309             String text,
310             int lineNo,
311             SuppressionType suppressionType,
312             SuppressWithPlainTextCommentFilter filter
313         ) {
314             this.lineNo = lineNo;
315             this.suppressionType = suppressionType;
316 
317             final Pattern commentFormat;
318             if (this.suppressionType == SuppressionType.ON) {
319                 commentFormat = filter.onCommentFormat;
320             }
321             else {
322                 commentFormat = filter.offCommentFormat;
323             }
324 
325             // Expand regexp for check and message
326             // Does not intern Patterns with Utils.getPattern()
327             String format = "";
328             try {
329                 format = CommonUtil.fillTemplateWithStringsByRegexp(
330                         filter.checkFormat, text, commentFormat);
331                 eventSourceRegexp = Pattern.compile(format);
332                 if (filter.messageFormat == null) {
333                     eventMessageRegexp = null;
334                 }
335                 else {
336                     format = CommonUtil.fillTemplateWithStringsByRegexp(
337                             filter.messageFormat, text, commentFormat);
338                     eventMessageRegexp = Pattern.compile(format);
339                 }
340                 if (filter.idFormat == null) {
341                     eventIdRegexp = null;
342                 }
343                 else {
344                     format = CommonUtil.fillTemplateWithStringsByRegexp(
345                             filter.idFormat, text, commentFormat);
346                     eventIdRegexp = Pattern.compile(format);
347                 }
348             }
349             catch (final PatternSyntaxException exc) {
350                 throw new IllegalArgumentException(
351                     "unable to parse expanded comment " + format, exc);
352             }
353         }
354 
355         /**
356          * Indicates whether some other object is "equal to" this one.
357          *
358          * @noinspection EqualsCalledOnEnumConstant
359          * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
360          *      code consistent
361          */
362         @Override
363         public boolean equals(Object other) {
364             if (this == other) {
365                 return true;
366             }
367             if (other == null || getClass() != other.getClass()) {
368                 return false;
369             }
370             final Suppression suppression = (Suppression) other;
371             return lineNo == suppression.lineNo
372                     && Objects.equals(suppressionType, suppression.suppressionType)
373                     && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
374                     && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp)
375                     && Objects.equals(eventIdRegexp, suppression.eventIdRegexp);
376         }
377 
378         @Override
379         public int hashCode() {
380             return Objects.hash(
381                 lineNo, suppressionType, eventSourceRegexp, eventMessageRegexp,
382                 eventIdRegexp);
383         }
384 
385         /**
386          * Checks whether the suppression matches the given {@link AuditEvent}.
387          *
388          * @param event {@link AuditEvent} instance.
389          * @return true if the suppression matches {@link AuditEvent}.
390          */
391         private boolean isMatch(AuditEvent event) {
392             return isInScopeOfSuppression(event)
393                     && isCheckMatch(event)
394                     && isIdMatch(event)
395                     && isMessageMatch(event);
396         }
397 
398         /**
399          * Checks whether {@link AuditEvent} is in the scope of the suppression.
400          *
401          * @param event {@link AuditEvent} instance.
402          * @return true if {@link AuditEvent} is in the scope of the suppression.
403          */
404         private boolean isInScopeOfSuppression(AuditEvent event) {
405             return lineNo <= event.getLine();
406         }
407 
408         /**
409          * Checks whether {@link AuditEvent} source name matches the check format.
410          *
411          * @param event {@link AuditEvent} instance.
412          * @return true if the {@link AuditEvent} source name matches the check format.
413          */
414         private boolean isCheckMatch(AuditEvent event) {
415             final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
416             return checkMatcher.find();
417         }
418 
419         /**
420          * Checks whether the {@link AuditEvent} module ID matches the ID format.
421          *
422          * @param event {@link AuditEvent} instance.
423          * @return true if the {@link AuditEvent} module ID matches the ID format.
424          */
425         private boolean isIdMatch(AuditEvent event) {
426             boolean match = true;
427             if (eventIdRegexp != null) {
428                 if (event.getModuleId() == null) {
429                     match = false;
430                 }
431                 else {
432                     final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
433                     match = idMatcher.find();
434                 }
435             }
436             return match;
437         }
438 
439         /**
440          * Checks whether the {@link AuditEvent} message matches the message format.
441          *
442          * @param event {@link AuditEvent} instance.
443          * @return true if the {@link AuditEvent} message matches the message format.
444          */
445         private boolean isMessageMatch(AuditEvent event) {
446             boolean match = true;
447             if (eventMessageRegexp != null) {
448                 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
449                 match = messageMatcher.find();
450             }
451             return match;
452         }
453     }
454 
455 }