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