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