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.IOException;
23  import java.nio.charset.StandardCharsets;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.List;
30  import java.util.Optional;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  import java.util.regex.PatternSyntaxException;
34  
35  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
36  import com.puppycrawl.tools.checkstyle.PropertyType;
37  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
38  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
39  import com.puppycrawl.tools.checkstyle.api.FileText;
40  import com.puppycrawl.tools.checkstyle.api.Filter;
41  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
42  
43  /**
44   * <div>
45   * Filter {@code SuppressWithNearbyTextFilter} uses plain text to suppress
46   * nearby audit events. The filter can suppress all checks which have Checker as a parent module.
47   * </div>
48   *
49   * <p>
50   * Notes:
51   * Setting {@code .*} value to {@code nearbyTextPattern} property will see <b>any</b>
52   * text as a suppression and will likely suppress all audit events in the file. It is
53   * best to set this to a key phrase not commonly used in the file to help denote it
54   * out of the rest of the file as a suppression. See the default value as an example.
55   * </p>
56   * <ul>
57   * <li>
58   * Property {@code checkPattern} - Specify check name pattern to suppress.
59   * Property can also be a RegExp group index at {@code nearbyTextPattern} in
60   * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
61   * Type is {@code java.util.regex.Pattern}.
62   * Default value is {@code ".*"}.
63   * </li>
64   * <li>
65   * Property {@code idPattern} - Specify check ID pattern to suppress.
66   * Type is {@code java.util.regex.Pattern}.
67   * Default value is {@code null}.
68   * </li>
69   * <li>
70   * Property {@code lineRange} - Specify negative/zero/positive value that
71   * defines the number of lines preceding/at/following the suppressing nearby text.
72   * Property can also be a RegExp group index at {@code nearbyTextPattern} in
73   * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
74   * Type is {@code java.lang.String}.
75   * Default value is {@code "0"}.
76   * </li>
77   * <li>
78   * Property {@code messagePattern} - Specify check violation message pattern to suppress.
79   * Type is {@code java.util.regex.Pattern}.
80   * Default value is {@code null}.
81   * </li>
82   * <li>
83   * Property {@code nearbyTextPattern} - Specify nearby text
84   * pattern to trigger filter to begin suppression.
85   * Type is {@code java.util.regex.Pattern}.
86   * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}.
87   * </li>
88   * </ul>
89   *
90   * <p>
91   * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
92   * </p>
93   *
94   * @since 10.10.0
95   */
96  public class SuppressWithNearbyTextFilter extends AbstractAutomaticBean implements Filter {
97  
98      /** Default nearby text pattern to turn check reporting off. */
99      private static final String DEFAULT_NEARBY_TEXT_PATTERN = "SUPPRESS CHECKSTYLE (\\w+)";
100 
101     /** Default regex for checks that should be suppressed. */
102     private static final String DEFAULT_CHECK_PATTERN = ".*";
103 
104     /** Default number of lines that should be suppressed. */
105     private static final String DEFAULT_LINE_RANGE = "0";
106 
107     /** Suppressions encountered in current file. */
108     private final List<Suppression> suppressions = new ArrayList<>();
109 
110     /** Specify nearby text pattern to trigger filter to begin suppression. */
111     @XdocsPropertyType(PropertyType.PATTERN)
112     private Pattern nearbyTextPattern = Pattern.compile(DEFAULT_NEARBY_TEXT_PATTERN);
113 
114     /**
115      * Specify check name pattern to suppress. Property can also be a RegExp group index
116      * at {@code nearbyTextPattern} in format of {@code $x} and be picked from line that
117      * matches {@code nearbyTextPattern}.
118      */
119     @XdocsPropertyType(PropertyType.PATTERN)
120     private String checkPattern = DEFAULT_CHECK_PATTERN;
121 
122     /** Specify check violation message pattern to suppress. */
123     @XdocsPropertyType(PropertyType.PATTERN)
124     private String messagePattern;
125 
126     /** Specify check ID pattern to suppress. */
127     @XdocsPropertyType(PropertyType.PATTERN)
128     private String idPattern;
129 
130     /**
131      * Specify negative/zero/positive value that defines the number of lines
132      * preceding/at/following the suppressing nearby text. Property can also be a RegExp group
133      * index at {@code nearbyTextPattern} in format of {@code $x} and be picked
134      * from line that matches {@code nearbyTextPattern}.
135      */
136     private String lineRange = DEFAULT_LINE_RANGE;
137 
138     /** The absolute path to the currently processed file. */
139     private String cachedFileAbsolutePath = "";
140 
141     /**
142      * Setter to specify nearby text pattern to trigger filter to begin suppression.
143      *
144      * @param pattern a {@code Pattern} value.
145      * @since 10.10.0
146      */
147     public final void setNearbyTextPattern(Pattern pattern) {
148         nearbyTextPattern = pattern;
149     }
150 
151     /**
152      * Setter to specify check name pattern to suppress. Property can also
153      * be a RegExp group index at {@code nearbyTextPattern} in
154      * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
155      *
156      * @param pattern a {@code String} value.
157      * @since 10.10.0
158      */
159     public final void setCheckPattern(String pattern) {
160         checkPattern = pattern;
161     }
162 
163     /**
164      * Setter to specify check violation message pattern to suppress.
165      *
166      * @param pattern a {@code String} value.
167      * @since 10.10.0
168      */
169     public void setMessagePattern(String pattern) {
170         messagePattern = pattern;
171     }
172 
173     /**
174      * Setter to specify check ID pattern to suppress.
175      *
176      * @param pattern a {@code String} value.
177      * @since 10.10.0
178      */
179     public void setIdPattern(String pattern) {
180         idPattern = pattern;
181     }
182 
183     /**
184      * Setter to specify negative/zero/positive value that defines the number
185      * of lines preceding/at/following the suppressing nearby text. Property can also
186      * be a RegExp group index at {@code nearbyTextPattern} in
187      * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
188      *
189      * @param format a {@code String} value.
190      * @since 10.10.0
191      */
192     public final void setLineRange(String format) {
193         lineRange = format;
194     }
195 
196     @Override
197     public boolean accept(AuditEvent event) {
198         boolean accepted = true;
199 
200         if (event.getViolation() != null) {
201             final String eventFileTextAbsolutePath = event.getFileName();
202 
203             if (!cachedFileAbsolutePath.equals(eventFileTextAbsolutePath)) {
204                 final FileText currentFileText = getFileText(eventFileTextAbsolutePath);
205 
206                 if (currentFileText != null) {
207                     cachedFileAbsolutePath = currentFileText.getFile().getAbsolutePath();
208                     collectSuppressions(currentFileText);
209                 }
210             }
211 
212             final Optional<Suppression> nearestSuppression =
213                     getNearestSuppression(suppressions, event);
214             accepted = nearestSuppression.isEmpty();
215         }
216         return accepted;
217     }
218 
219     @Override
220     protected void finishLocalSetup() {
221         // No code by default
222     }
223 
224     /**
225      * Returns {@link FileText} instance created based on the given file name.
226      *
227      * @param fileName the name of the file.
228      * @return {@link FileText} instance.
229      * @throws IllegalStateException if the file could not be read.
230      */
231     private static FileText getFileText(String fileName) {
232         final Path path = Paths.get(fileName);
233         FileText result = null;
234 
235         // some violations can be on a directory, instead of a file
236         if (!Files.isDirectory(path)) {
237             try {
238                 result = new FileText(path.toFile(), StandardCharsets.UTF_8.name());
239             }
240             catch (IOException exc) {
241                 throw new IllegalStateException("Cannot read source file: " + fileName, exc);
242             }
243         }
244 
245         return result;
246     }
247 
248     /**
249      * Collets all {@link Suppression} instances retrieved from the given {@link FileText}.
250      *
251      * @param fileText {@link FileText} instance.
252      */
253     private void collectSuppressions(FileText fileText) {
254         suppressions.clear();
255 
256         for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
257             final Suppression suppression = getSuppression(fileText, lineNo);
258             if (suppression != null) {
259                 suppressions.add(suppression);
260             }
261         }
262     }
263 
264     /**
265      * Tries to extract the suppression from the given line.
266      *
267      * @param fileText {@link FileText} instance.
268      * @param lineNo line number.
269      * @return {@link Suppression} instance.
270      */
271     private Suppression getSuppression(FileText fileText, int lineNo) {
272         final String line = fileText.get(lineNo);
273         final Matcher nearbyTextMatcher = nearbyTextPattern.matcher(line);
274 
275         Suppression suppression = null;
276         if (nearbyTextMatcher.find()) {
277             final String text = nearbyTextMatcher.group(0);
278             suppression = new Suppression(text, lineNo + 1, this);
279         }
280 
281         return suppression;
282     }
283 
284     /**
285      * Finds the nearest {@link Suppression} instance which can suppress
286      * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
287      * is before the line and column of the event.
288      *
289      * @param suppressions collection of {@link Suppression} instances.
290      * @param event {@link AuditEvent} instance.
291      * @return {@link Suppression} instance.
292      */
293     private static Optional<Suppression> getNearestSuppression(Collection<Suppression> suppressions,
294                                                                AuditEvent event) {
295         return suppressions
296                 .stream()
297                 .filter(suppression -> suppression.isMatch(event))
298                 .findFirst();
299     }
300 
301     /** The class which represents the suppression. */
302     private static final class Suppression {
303 
304         /** The first line where warnings may be suppressed. */
305         private final int firstLine;
306 
307         /** The last line where warnings may be suppressed. */
308         private final int lastLine;
309 
310         /** The regexp which is used to match the event source.*/
311         private final Pattern eventSourceRegexp;
312 
313         /** The regexp which is used to match the event message.*/
314         private Pattern eventMessageRegexp;
315 
316         /** The regexp which is used to match the event ID.*/
317         private Pattern eventIdRegexp;
318 
319         /**
320          * Constructs new {@code Suppression} instance.
321          *
322          * @param text suppression text.
323          * @param lineNo suppression line number.
324          * @param filter the {@code SuppressWithNearbyTextFilter} with the context.
325          * @throws IllegalArgumentException if there is an error in the filter regex syntax.
326          */
327         private Suppression(
328                 String text,
329                 int lineNo,
330                 SuppressWithNearbyTextFilter filter
331         ) {
332             final Pattern nearbyTextPattern = filter.nearbyTextPattern;
333             final String lineRange = filter.lineRange;
334             String format = "";
335             try {
336                 format = CommonUtil.fillTemplateWithStringsByRegexp(
337                         filter.checkPattern, text, nearbyTextPattern);
338                 eventSourceRegexp = Pattern.compile(format);
339                 if (filter.messagePattern != null) {
340                     format = CommonUtil.fillTemplateWithStringsByRegexp(
341                             filter.messagePattern, text, nearbyTextPattern);
342                     eventMessageRegexp = Pattern.compile(format);
343                 }
344                 if (filter.idPattern != null) {
345                     format = CommonUtil.fillTemplateWithStringsByRegexp(
346                             filter.idPattern, text, nearbyTextPattern);
347                     eventIdRegexp = Pattern.compile(format);
348                 }
349                 format = CommonUtil.fillTemplateWithStringsByRegexp(lineRange,
350                                                                     text, nearbyTextPattern);
351 
352                 final int range = parseRange(format, lineRange, text);
353 
354                 firstLine = Math.min(lineNo, lineNo + range);
355                 lastLine = Math.max(lineNo, lineNo + range);
356             }
357             catch (final PatternSyntaxException exc) {
358                 throw new IllegalArgumentException(
359                     "unable to parse expanded comment " + format, exc);
360             }
361         }
362 
363         /**
364          * Gets range from suppress filter range format param.
365          *
366          * @param format range format to parse
367          * @param lineRange raw line range
368          * @param text text of the suppression
369          * @return parsed range
370          * @throws IllegalArgumentException when unable to parse int in format
371          */
372         private static int parseRange(String format, String lineRange, String text) {
373             try {
374                 return Integer.parseInt(format);
375             }
376             catch (final NumberFormatException exc) {
377                 throw new IllegalArgumentException("unable to parse line range from '" + text
378                         + "' using " + lineRange, exc);
379             }
380         }
381 
382         /**
383          * Determines whether the source of an audit event
384          * matches the text of this suppression.
385          *
386          * @param event the {@code AuditEvent} to check.
387          * @return true if the source of event matches the text of this suppression.
388          */
389         private boolean isMatch(AuditEvent event) {
390             return isInScopeOfSuppression(event)
391                     && isCheckMatch(event)
392                     && isIdMatch(event)
393                     && isMessageMatch(event);
394         }
395 
396         /**
397          * Checks whether the {@link AuditEvent} is in the scope of the suppression.
398          *
399          * @param event {@link AuditEvent} instance.
400          * @return true if the {@link AuditEvent} is in the scope of the suppression.
401          */
402         private boolean isInScopeOfSuppression(AuditEvent event) {
403             final int eventLine = event.getLine();
404             return eventLine >= firstLine && eventLine <= lastLine;
405         }
406 
407         /**
408          * Checks whether {@link AuditEvent} source name matches the check pattern.
409          *
410          * @param event {@link AuditEvent} instance.
411          * @return true if the {@link AuditEvent} source name matches the check pattern.
412          */
413         private boolean isCheckMatch(AuditEvent event) {
414             final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
415             return checkMatcher.find();
416         }
417 
418         /**
419          * Checks whether the {@link AuditEvent} module ID matches the ID pattern.
420          *
421          * @param event {@link AuditEvent} instance.
422          * @return true if the {@link AuditEvent} module ID matches the ID pattern.
423          */
424         private boolean isIdMatch(AuditEvent event) {
425             boolean match = true;
426             if (eventIdRegexp != null) {
427                 if (event.getModuleId() == null) {
428                     match = false;
429                 }
430                 else {
431                     final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
432                     match = idMatcher.find();
433                 }
434             }
435             return match;
436         }
437 
438         /**
439          * Checks whether the {@link AuditEvent} message matches the message pattern.
440          *
441          * @param event {@link AuditEvent} instance.
442          * @return true if the {@link AuditEvent} message matches the message pattern.
443          */
444         private boolean isMessageMatch(AuditEvent event) {
445             boolean match = true;
446             if (eventMessageRegexp != null) {
447                 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
448                 match = messageMatcher.find();
449             }
450             return match;
451         }
452     }
453 }