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