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