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