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