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