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