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