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