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 * Creates a new {@code SuppressWithNearbyCommentFilter} instance.
127 */
128 public SuppressWithNearbyCommentFilter() {
129 // no code by default
130 }
131
132 /**
133 * Setter to specify comment pattern to trigger filter to begin suppression.
134 *
135 * @param pattern a pattern.
136 * @since 5.0
137 */
138 public final void setCommentFormat(Pattern pattern) {
139 commentFormat = pattern;
140 }
141
142 /**
143 * Setter to specify check pattern to suppress.
144 * The pattern is matched against the fully qualified class name of the Check.
145 *
146 * @param format a {@code String} value
147 * @since 5.0
148 */
149 public final void setCheckFormat(String format) {
150 checkFormat = format;
151 }
152
153 /**
154 * Setter to define message pattern to suppress.
155 *
156 * @param format a {@code String} value
157 * @since 5.0
158 */
159 public void setMessageFormat(String format) {
160 messageFormat = format;
161 }
162
163 /**
164 * Setter to specify check ID pattern to suppress.
165 *
166 * @param format a {@code String} value
167 * @since 8.24
168 */
169 public void setIdFormat(String format) {
170 idFormat = format;
171 }
172
173 /**
174 * Setter to specify negative/zero/positive value that defines the number
175 * of lines preceding/at/following the suppression comment.
176 *
177 * @param format a {@code String} value
178 * @since 5.0
179 */
180 public final void setInfluenceFormat(String format) {
181 influenceFormat = format;
182 }
183
184 /**
185 * Setter to control whether to check C++ style comments ({@code //}).
186 *
187 * @param checkCppComments {@code true} if C++ comments are checked.
188 * @since 5.0
189 */
190 // -@cs[AbbreviationAsWordInName] We can not change it as,
191 // check's property is a part of API (used in configurations).
192 public void setCheckCPP(boolean checkCppComments) {
193 checkCPP = checkCppComments;
194 }
195
196 /**
197 * Setter to control whether to check C style comments (/* ... */).
198 *
199 * @param checkC {@code true} if C comments are checked.
200 * @since 5.0
201 */
202 public void setCheckC(boolean checkC) {
203 this.checkC = checkC;
204 }
205
206 @Override
207 protected void finishLocalSetup() {
208 // No code by default
209 }
210
211 @Override
212 public boolean accept(TreeWalkerAuditEvent event) {
213 boolean accepted = true;
214
215 if (event.violation() != null) {
216 fileContentsHolder.lazyUpdate(event.fileContents(), this::tagSuppressions);
217 if (matchesTag(event)) {
218 accepted = false;
219 }
220 }
221 return accepted;
222 }
223
224 /**
225 * Whether current event matches any tag from {@link #tags}.
226 *
227 * @param event TreeWalkerAuditEvent to test match on {@code #tags}.
228 * @return true if event matches any tag from {@code #tags}, false otherwise.
229 */
230 private boolean matchesTag(TreeWalkerAuditEvent event) {
231 boolean result = false;
232 for (final Tag tag : tags) {
233 if (tag.isMatch(event)) {
234 result = true;
235 break;
236 }
237 }
238 return result;
239 }
240
241 /**
242 * Collects all the suppression tags for all comments into a list and
243 * sorts the list.
244 */
245 private void tagSuppressions() {
246 tags.clear();
247 final FileContents contents = fileContentsHolder.get();
248 if (checkCPP) {
249 tagSuppressions(contents.getSingleLineComments().values());
250 }
251 if (checkC) {
252 final Collection<List<TextBlock>> cComments =
253 contents.getBlockComments().values();
254 cComments.forEach(this::tagSuppressions);
255 }
256 }
257
258 /**
259 * Appends the suppressions in a collection of comments to the full
260 * set of suppression tags.
261 *
262 * @param comments the set of comments.
263 */
264 private void tagSuppressions(Collection<TextBlock> comments) {
265 for (final TextBlock comment : comments) {
266 final int startLineNo = comment.getStartLineNo();
267 final String[] text = comment.getText();
268 tagCommentLine(text[0], startLineNo);
269 for (int i = 1; i < text.length; i++) {
270 tagCommentLine(text[i], startLineNo + i);
271 }
272 }
273 }
274
275 /**
276 * Tags a string if it matches the format for turning
277 * checkstyle reporting on or the format for turning reporting off.
278 *
279 * @param text the string to tag.
280 * @param line the line number of text.
281 */
282 private void tagCommentLine(String text, int line) {
283 final Matcher matcher = commentFormat.matcher(text);
284 if (matcher.find()) {
285 addTag(matcher.group(0), line);
286 }
287 }
288
289 /**
290 * Adds a comment suppression {@code Tag} to the list of all tags.
291 *
292 * @param text the text of the tag.
293 * @param line the line number of the tag.
294 */
295 private void addTag(String text, int line) {
296 final Tag tag = new Tag(text, line, this);
297 tags.add(tag);
298 }
299
300 /**
301 * A Tag holds a suppression comment and its location.
302 */
303 private static final class Tag {
304
305 /** The text of the tag. */
306 private final String text;
307
308 /** The first line where warnings may be suppressed. */
309 private final int firstLine;
310
311 /** The last line where warnings may be suppressed. */
312 private final int lastLine;
313
314 /** The parsed check regexp, expanded for the text of this tag. */
315 private final Pattern tagCheckRegexp;
316
317 /** The parsed message regexp, expanded for the text of this tag. */
318 private final Pattern tagMessageRegexp;
319
320 /** The parsed check ID regexp, expanded for the text of this tag. */
321 private final Pattern tagIdRegexp;
322
323 /**
324 * Constructs a tag.
325 *
326 * @param text the text of the suppression.
327 * @param line the line number.
328 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
329 * @throws IllegalArgumentException if unable to parse expanded text.
330 */
331 private Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
332 this.text = text;
333
334 // Expand regexp for check and message
335 // Does not intern Patterns with Utils.getPattern()
336 String format = "";
337 try {
338 format = CommonUtil.fillTemplateWithStringsByRegexp(
339 filter.checkFormat, text, filter.commentFormat);
340 tagCheckRegexp = Pattern.compile(format);
341 if (filter.messageFormat == null) {
342 tagMessageRegexp = null;
343 }
344 else {
345 format = CommonUtil.fillTemplateWithStringsByRegexp(
346 filter.messageFormat, text, filter.commentFormat);
347 tagMessageRegexp = Pattern.compile(format);
348 }
349 if (filter.idFormat == null) {
350 tagIdRegexp = null;
351 }
352 else {
353 format = CommonUtil.fillTemplateWithStringsByRegexp(
354 filter.idFormat, text, filter.commentFormat);
355 tagIdRegexp = Pattern.compile(format);
356 }
357 format = CommonUtil.fillTemplateWithStringsByRegexp(
358 filter.influenceFormat, text, filter.commentFormat);
359
360 final int influence = parseInfluence(format, filter.influenceFormat, text);
361
362 if (influence >= 1) {
363 firstLine = line;
364 lastLine = line + influence;
365 }
366 else {
367 firstLine = line + influence;
368 lastLine = line;
369 }
370 }
371 catch (final PatternSyntaxException exc) {
372 throw new IllegalArgumentException(
373 "unable to parse expanded comment " + format, exc);
374 }
375 }
376
377 /**
378 * Gets influence from suppress filter influence format param.
379 *
380 * @param format influence format to parse
381 * @param influenceFormat raw influence format
382 * @param text text of the suppression
383 * @return parsed influence
384 * @throws IllegalArgumentException when unable to parse int in format
385 */
386 private static int parseInfluence(String format, String influenceFormat, String text) {
387 try {
388 return Integer.parseInt(format);
389 }
390 catch (final NumberFormatException exc) {
391 throw new IllegalArgumentException("unable to parse influence from '" + text
392 + "' using " + influenceFormat, exc);
393 }
394 }
395
396 @Override
397 public boolean equals(Object other) {
398 if (this == other) {
399 return true;
400 }
401 if (other == null || getClass() != other.getClass()) {
402 return false;
403 }
404 final Tag tag = (Tag) other;
405 return firstLine == tag.firstLine
406 && lastLine == tag.lastLine
407 && Objects.equals(text, tag.text)
408 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
409 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
410 && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
411 }
412
413 @Override
414 public int hashCode() {
415 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp,
416 tagIdRegexp);
417 }
418
419 /**
420 * Determines whether the source of an audit event
421 * matches the text of this tag.
422 *
423 * @param event the {@code TreeWalkerAuditEvent} to check.
424 * @return true if the source of event matches the text of this tag.
425 */
426 /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
427 return isInScopeOfSuppression(event)
428 && isCheckMatch(event)
429 && isIdMatch(event)
430 && isMessageMatch(event);
431 }
432
433 /**
434 * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
435 *
436 * @param event {@code TreeWalkerAuditEvent} instance.
437 * @return true if the {@code TreeWalkerAuditEvent} is in the scope of the suppression.
438 */
439 private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) {
440 final int line = event.getLine();
441 return line >= firstLine && line <= lastLine;
442 }
443
444 /**
445 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
446 *
447 * @param event {@code TreeWalkerAuditEvent} instance.
448 * @return true if the {@code TreeWalkerAuditEvent} source name matches the check format.
449 */
450 private boolean isCheckMatch(TreeWalkerAuditEvent event) {
451 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
452 return checkMatcher.find();
453 }
454
455 /**
456 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
457 *
458 * @param event {@code TreeWalkerAuditEvent} instance.
459 * @return true if the {@code TreeWalkerAuditEvent} module ID matches the ID format.
460 */
461 private boolean isIdMatch(TreeWalkerAuditEvent event) {
462 boolean match = true;
463 if (tagIdRegexp != null) {
464 if (event.getModuleId() == null) {
465 match = false;
466 }
467 else {
468 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
469 match = idMatcher.find();
470 }
471 }
472 return match;
473 }
474
475 /**
476 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
477 *
478 * @param event {@code TreeWalkerAuditEvent} instance.
479 * @return true if the {@code TreeWalkerAuditEvent} message matches the message format.
480 */
481 private boolean isMessageMatch(TreeWalkerAuditEvent event) {
482 boolean match = true;
483 if (tagMessageRegexp != null) {
484 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
485 match = messageMatcher.find();
486 }
487 return match;
488 }
489
490 @Override
491 public String toString() {
492 return "Tag[text='" + text + '\''
493 + ", firstLine=" + firstLine
494 + ", lastLine=" + lastLine
495 + ", tagCheckRegexp=" + tagCheckRegexp
496 + ", tagMessageRegexp=" + tagMessageRegexp
497 + ", tagIdRegexp=" + tagIdRegexp
498 + ']';
499 }
500
501 }
502
503 }