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