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