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