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.util.ArrayList;
23 import java.util.Collection;
24 import java.util.List;
25 import java.util.Objects;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import java.util.regex.PatternSyntaxException;
29
30 import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
31 import com.puppycrawl.tools.checkstyle.PropertyType;
32 import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
33 import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
34 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
35 import com.puppycrawl.tools.checkstyle.api.FileContents;
36 import com.puppycrawl.tools.checkstyle.api.TextBlock;
37 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38 import com.puppycrawl.tools.checkstyle.utils.WeakReferenceHolder;
39
40 /**
41 * <div>
42 * Filter {@code SuppressWithNearbyCommentFilter} uses nearby comments to suppress audit events.
43 * </div>
44 *
45 * <p>
46 * Rationale: Same as {@code SuppressionCommentFilter}.
47 * Whereas the SuppressionCommentFilter uses matched pairs of filters to turn
48 * on/off comment matching, {@code SuppressWithNearbyCommentFilter} uses single comments.
49 * This requires fewer lines to mark a region, and may be aesthetically preferable in some contexts.
50 * </p>
51 *
52 * <p>
53 * Attention: This filter may only be specified within the TreeWalker module
54 * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also
55 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline},
56 * a
57 * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html">
58 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
59 * </p>
60 *
61 * <p>
62 * Notes:
63 * SuppressWithNearbyCommentFilter can suppress Checks that have
64 * Treewalker as parent module.
65 * </p>
66 *
67 * @since 5.0
68 */
69 public class SuppressWithNearbyCommentFilter
70 extends AbstractAutomaticBean
71 implements TreeWalkerFilter {
72
73 /** Format to turn checkstyle reporting off. */
74 private static final String DEFAULT_COMMENT_FORMAT =
75 "SUPPRESS CHECKSTYLE (\\w+)";
76
77 /** Default regex for checks that should be suppressed. */
78 private static final String DEFAULT_CHECK_FORMAT = ".*";
79
80 /** Default regex for lines that should be suppressed. */
81 private static final String DEFAULT_INFLUENCE_FORMAT = "0";
82
83 /** Tagged comments. */
84 private final List<Tag> tags = new ArrayList<>();
85
86 /**
87 * References the current FileContents for this filter.
88 * Since this is a weak reference to the FileContents, the FileContents
89 * can be reclaimed as soon as the strong references in TreeWalker
90 * are reassigned to the next FileContents, at which time filtering for
91 * the current FileContents is finished.
92 */
93 private final WeakReferenceHolder<FileContents> fileContentsHolder =
94 new WeakReferenceHolder<>();
95
96 /** Control whether to check C style comments (/* ... */). */
97 private boolean checkC = true;
98
99 /** Control whether to check C++ style comments ({@code //}). */
100 // -@cs[AbbreviationAsWordInName] We can not change it as,
101 // check's property is a part of API (used in configurations).
102 private boolean checkCPP = true;
103
104 /** Specify comment pattern to trigger filter to begin suppression. */
105 private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
106
107 /** Specify check pattern to suppress. */
108 @XdocsPropertyType(PropertyType.PATTERN)
109 private String checkFormat = DEFAULT_CHECK_FORMAT;
110
111 /** Define message pattern to suppress. */
112 @XdocsPropertyType(PropertyType.PATTERN)
113 private String messageFormat;
114
115 /** Specify check ID pattern to suppress. */
116 @XdocsPropertyType(PropertyType.PATTERN)
117 private String idFormat;
118
119 /**
120 * Specify negative/zero/positive value that defines the number of lines
121 * preceding/at/following the suppression comment.
122 */
123 private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
124
125 /**
126 * Setter to specify comment pattern to trigger filter to begin suppression.
127 *
128 * @param pattern a pattern.
129 * @since 5.0
130 */
131 public final void setCommentFormat(Pattern pattern) {
132 commentFormat = pattern;
133 }
134
135 /**
136 * Setter to specify check pattern to suppress.
137 * The pattern is matched against the fully qualified class name of the Check.
138 *
139 * @param format a {@code String} value
140 * @since 5.0
141 */
142 public final void setCheckFormat(String format) {
143 checkFormat = format;
144 }
145
146 /**
147 * Setter to define message pattern to suppress.
148 *
149 * @param format a {@code String} value
150 * @since 5.0
151 */
152 public void setMessageFormat(String format) {
153 messageFormat = format;
154 }
155
156 /**
157 * Setter to specify check ID pattern to suppress.
158 *
159 * @param format a {@code String} value
160 * @since 8.24
161 */
162 public void setIdFormat(String format) {
163 idFormat = format;
164 }
165
166 /**
167 * Setter to specify negative/zero/positive value that defines the number
168 * of lines preceding/at/following the suppression comment.
169 *
170 * @param format a {@code String} value
171 * @since 5.0
172 */
173 public final void setInfluenceFormat(String format) {
174 influenceFormat = format;
175 }
176
177 /**
178 * Setter to control whether to check C++ style comments ({@code //}).
179 *
180 * @param checkCppComments {@code true} if C++ comments are checked.
181 * @since 5.0
182 */
183 // -@cs[AbbreviationAsWordInName] We can not change it as,
184 // check's property is a part of API (used in configurations).
185 public void setCheckCPP(boolean checkCppComments) {
186 checkCPP = checkCppComments;
187 }
188
189 /**
190 * Setter to control whether to check C style comments (/* ... */).
191 *
192 * @param checkC {@code true} if C comments are checked.
193 * @since 5.0
194 */
195 public void setCheckC(boolean checkC) {
196 this.checkC = checkC;
197 }
198
199 @Override
200 protected void finishLocalSetup() {
201 // No code by default
202 }
203
204 @Override
205 public boolean accept(TreeWalkerAuditEvent event) {
206 boolean accepted = true;
207
208 if (event.violation() != null) {
209 fileContentsHolder.lazyUpdate(event.fileContents(), this::tagSuppressions);
210 if (matchesTag(event)) {
211 accepted = false;
212 }
213 }
214 return accepted;
215 }
216
217 /**
218 * Whether current event matches any tag from {@link #tags}.
219 *
220 * @param event TreeWalkerAuditEvent to test match on {@link #tags}.
221 * @return true if event matches any tag from {@link #tags}, false otherwise.
222 */
223 private boolean matchesTag(TreeWalkerAuditEvent event) {
224 boolean result = false;
225 for (final Tag tag : tags) {
226 if (tag.isMatch(event)) {
227 result = true;
228 break;
229 }
230 }
231 return result;
232 }
233
234 /**
235 * Collects all the suppression tags for all comments into a list and
236 * sorts the list.
237 */
238 private void tagSuppressions() {
239 tags.clear();
240 final FileContents contents = fileContentsHolder.get();
241 if (checkCPP) {
242 tagSuppressions(contents.getSingleLineComments().values());
243 }
244 if (checkC) {
245 final Collection<List<TextBlock>> cComments =
246 contents.getBlockComments().values();
247 cComments.forEach(this::tagSuppressions);
248 }
249 }
250
251 /**
252 * Appends the suppressions in a collection of comments to the full
253 * set of suppression tags.
254 *
255 * @param comments the set of comments.
256 */
257 private void tagSuppressions(Collection<TextBlock> comments) {
258 for (final TextBlock comment : comments) {
259 final int startLineNo = comment.getStartLineNo();
260 final String[] text = comment.getText();
261 tagCommentLine(text[0], startLineNo);
262 for (int i = 1; i < text.length; i++) {
263 tagCommentLine(text[i], startLineNo + i);
264 }
265 }
266 }
267
268 /**
269 * Tags a string if it matches the format for turning
270 * checkstyle reporting on or the format for turning reporting off.
271 *
272 * @param text the string to tag.
273 * @param line the line number of text.
274 */
275 private void tagCommentLine(String text, int line) {
276 final Matcher matcher = commentFormat.matcher(text);
277 if (matcher.find()) {
278 addTag(matcher.group(0), line);
279 }
280 }
281
282 /**
283 * Adds a comment suppression {@code Tag} to the list of all tags.
284 *
285 * @param text the text of the tag.
286 * @param line the line number of the tag.
287 */
288 private void addTag(String text, int line) {
289 final Tag tag = new Tag(text, line, this);
290 tags.add(tag);
291 }
292
293 /**
294 * A Tag holds a suppression comment and its location.
295 */
296 private static final class Tag {
297
298 /** The text of the tag. */
299 private final String text;
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 parsed check regexp, expanded for the text of this tag. */
308 private final Pattern tagCheckRegexp;
309
310 /** The parsed message regexp, expanded for the text of this tag. */
311 private final Pattern tagMessageRegexp;
312
313 /** The parsed check ID regexp, expanded for the text of this tag. */
314 private final Pattern tagIdRegexp;
315
316 /**
317 * Constructs a tag.
318 *
319 * @param text the text of the suppression.
320 * @param line the line number.
321 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
322 * @throws IllegalArgumentException if unable to parse expanded text.
323 */
324 private Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
325 this.text = text;
326
327 // Expand regexp for check and message
328 // Does not intern Patterns with Utils.getPattern()
329 String format = "";
330 try {
331 format = CommonUtil.fillTemplateWithStringsByRegexp(
332 filter.checkFormat, text, filter.commentFormat);
333 tagCheckRegexp = Pattern.compile(format);
334 if (filter.messageFormat == null) {
335 tagMessageRegexp = null;
336 }
337 else {
338 format = CommonUtil.fillTemplateWithStringsByRegexp(
339 filter.messageFormat, text, filter.commentFormat);
340 tagMessageRegexp = Pattern.compile(format);
341 }
342 if (filter.idFormat == null) {
343 tagIdRegexp = null;
344 }
345 else {
346 format = CommonUtil.fillTemplateWithStringsByRegexp(
347 filter.idFormat, text, filter.commentFormat);
348 tagIdRegexp = Pattern.compile(format);
349 }
350 format = CommonUtil.fillTemplateWithStringsByRegexp(
351 filter.influenceFormat, text, filter.commentFormat);
352
353 final int influence = parseInfluence(format, filter.influenceFormat, text);
354
355 if (influence >= 1) {
356 firstLine = line;
357 lastLine = line + influence;
358 }
359 else {
360 firstLine = line + influence;
361 lastLine = line;
362 }
363 }
364 catch (final PatternSyntaxException exc) {
365 throw new IllegalArgumentException(
366 "unable to parse expanded comment " + format, exc);
367 }
368 }
369
370 /**
371 * Gets influence from suppress filter influence format param.
372 *
373 * @param format influence format to parse
374 * @param influenceFormat raw influence format
375 * @param text text of the suppression
376 * @return parsed influence
377 * @throws IllegalArgumentException when unable to parse int in format
378 */
379 private static int parseInfluence(String format, String influenceFormat, String text) {
380 try {
381 return Integer.parseInt(format);
382 }
383 catch (final NumberFormatException exc) {
384 throw new IllegalArgumentException("unable to parse influence from '" + text
385 + "' using " + influenceFormat, exc);
386 }
387 }
388
389 @Override
390 public boolean equals(Object other) {
391 if (this == other) {
392 return true;
393 }
394 if (other == null || getClass() != other.getClass()) {
395 return false;
396 }
397 final Tag tag = (Tag) other;
398 return firstLine == tag.firstLine
399 && lastLine == tag.lastLine
400 && Objects.equals(text, tag.text)
401 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
402 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
403 && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
404 }
405
406 @Override
407 public int hashCode() {
408 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp,
409 tagIdRegexp);
410 }
411
412 /**
413 * Determines whether the source of an audit event
414 * matches the text of this tag.
415 *
416 * @param event the {@code TreeWalkerAuditEvent} to check.
417 * @return true if the source of event matches the text of this tag.
418 */
419 /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
420 return isInScopeOfSuppression(event)
421 && isCheckMatch(event)
422 && isIdMatch(event)
423 && isMessageMatch(event);
424 }
425
426 /**
427 * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
428 *
429 * @param event {@link TreeWalkerAuditEvent} instance.
430 * @return true if the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
431 */
432 private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) {
433 final int line = event.getLine();
434 return line >= firstLine && line <= lastLine;
435 }
436
437 /**
438 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
439 *
440 * @param event {@link TreeWalkerAuditEvent} instance.
441 * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
442 */
443 private boolean isCheckMatch(TreeWalkerAuditEvent event) {
444 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
445 return checkMatcher.find();
446 }
447
448 /**
449 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
450 *
451 * @param event {@link TreeWalkerAuditEvent} instance.
452 * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
453 */
454 private boolean isIdMatch(TreeWalkerAuditEvent event) {
455 boolean match = true;
456 if (tagIdRegexp != null) {
457 if (event.getModuleId() == null) {
458 match = false;
459 }
460 else {
461 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
462 match = idMatcher.find();
463 }
464 }
465 return match;
466 }
467
468 /**
469 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
470 *
471 * @param event {@link TreeWalkerAuditEvent} instance.
472 * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
473 */
474 private boolean isMessageMatch(TreeWalkerAuditEvent event) {
475 boolean match = true;
476 if (tagMessageRegexp != null) {
477 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
478 match = messageMatcher.find();
479 }
480 return match;
481 }
482
483 @Override
484 public String toString() {
485 return "Tag[text='" + text + '\''
486 + ", firstLine=" + firstLine
487 + ", lastLine=" + lastLine
488 + ", tagCheckRegexp=" + tagCheckRegexp
489 + ", tagMessageRegexp=" + tagMessageRegexp
490 + ", tagIdRegexp=" + tagIdRegexp
491 + ']';
492 }
493
494 }
495
496 }