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