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