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 }