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.Comparator;
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 import com.puppycrawl.tools.checkstyle.utils.WeakReferenceHolder;
40
41 /**
42 * <div>
43 * Filter {@code SuppressionCommentFilter} uses pairs of comments to suppress audit events.
44 * </div>
45 *
46 * <p>
47 * Rationale:
48 * Sometimes there are legitimate reasons for violating a check. When
49 * this is a matter of the code in question and not personal
50 * preference, the best place to override the policy is in the code
51 * itself. Semi-structured comments can be associated with the check.
52 * This is sometimes superior to a separate suppressions file, which
53 * must be kept up-to-date as the source file is edited.
54 * </p>
55 *
56 * <p>
57 * Note that the suppression comment should be put before the violation.
58 * You can use more than one suppression comment each on separate line.
59 * </p>
60 *
61 * <p>
62 * Attention: This filter may only be specified within the TreeWalker module
63 * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also
64 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, a
65 * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html">
66 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
67 * </p>
68 *
69 * <p>
70 * Notes:
71 * {@code offCommentFormat} and {@code onCommentFormat} must have equal
72 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
73 * paren counts</a>.
74 * </p>
75 *
76 * <p>
77 * SuppressionCommentFilter can suppress Checks that have Treewalker as parent module.
78 * </p>
79 *
80 * @since 3.5
81 */
82 public class SuppressionCommentFilter
83 extends AbstractAutomaticBean
84 implements TreeWalkerFilter {
85
86 /**
87 * Enum to be used for switching checkstyle reporting for tags.
88 */
89 public enum TagType {
90
91 /**
92 * Switch reporting on.
93 */
94 ON,
95 /**
96 * Switch reporting off.
97 */
98 OFF,
99
100 }
101
102 /** Turns checkstyle reporting off. */
103 private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
104
105 /** Turns checkstyle reporting on. */
106 private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
107
108 /** Control all checks. */
109 private static final String DEFAULT_CHECK_FORMAT = ".*";
110
111 /** Tagged comments. */
112 private final List<Tag> tags = new ArrayList<>();
113
114 /**
115 * References the current FileContents for this filter.
116 * Since this is a weak reference to the FileContents, the FileContents
117 * can be reclaimed as soon as the strong references in TreeWalker
118 * are reassigned to the next FileContents, at which time filtering for
119 * the current FileContents is finished.
120 */
121 private final WeakReferenceHolder<FileContents> fileContentsHolder =
122 new WeakReferenceHolder<>();
123
124 /** Control whether to check C style comments (/* ... */). */
125 private boolean checkC = true;
126
127 /** Control whether to check C++ style comments ({@code //}). */
128 // -@cs[AbbreviationAsWordInName] we can not change it as,
129 // Check property is a part of API (used in configurations)
130 private boolean checkCPP = true;
131
132 /** Specify comment pattern to trigger filter to begin suppression. */
133 private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
134
135 /** Specify comment pattern to trigger filter to end suppression. */
136 private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
137
138 /** Specify check pattern to suppress. */
139 @XdocsPropertyType(PropertyType.PATTERN)
140 private String checkFormat = DEFAULT_CHECK_FORMAT;
141
142 /** Specify message pattern to suppress. */
143 @XdocsPropertyType(PropertyType.PATTERN)
144 private String messageFormat;
145
146 /** Specify check ID pattern to suppress. */
147 @XdocsPropertyType(PropertyType.PATTERN)
148 private String idFormat;
149
150 /**
151 * Creates a new {@code SuppressionCommentFilter} instance.
152 */
153 public SuppressionCommentFilter() {
154 // no code by default
155 }
156
157 /**
158 * Setter to specify comment pattern to trigger filter to begin suppression.
159 *
160 * @param pattern a pattern.
161 * @since 3.5
162 */
163 public final void setOffCommentFormat(Pattern pattern) {
164 offCommentFormat = pattern;
165 }
166
167 /**
168 * Setter to specify comment pattern to trigger filter to end suppression.
169 *
170 * @param pattern a pattern.
171 * @since 3.5
172 */
173 public final void setOnCommentFormat(Pattern pattern) {
174 onCommentFormat = pattern;
175 }
176
177 /**
178 * Setter to specify check pattern to suppress.
179 * The pattern is matched against the fully qualified class name of the Check.
180 *
181 * @param format a {@code String} value
182 * @since 3.5
183 */
184 public final void setCheckFormat(String format) {
185 checkFormat = format;
186 }
187
188 /**
189 * Setter to specify message pattern to suppress.
190 *
191 * @param format a {@code String} value
192 * @since 3.5
193 */
194 public void setMessageFormat(String format) {
195 messageFormat = format;
196 }
197
198 /**
199 * Setter to specify check ID pattern to suppress.
200 *
201 * @param format a {@code String} value
202 * @since 8.24
203 */
204 public void setIdFormat(String format) {
205 idFormat = format;
206 }
207
208 /**
209 * Setter to control whether to check C++ style comments ({@code //}).
210 *
211 * @param checkCppComments {@code true} if C++ comments are checked.
212 * @since 3.5
213 */
214 // -@cs[AbbreviationAsWordInName] We can not change it as,
215 // check's property is a part of API (used in configurations).
216 public void setCheckCPP(boolean checkCppComments) {
217 checkCPP = checkCppComments;
218 }
219
220 /**
221 * Setter to control whether to check C style comments (/* ... */).
222 *
223 * @param checkC {@code true} if C comments are checked.
224 * @since 3.5
225 */
226 public void setCheckC(boolean checkC) {
227 this.checkC = checkC;
228 }
229
230 @Override
231 protected void finishLocalSetup() {
232 // No code by default
233 }
234
235 @Override
236 public boolean accept(TreeWalkerAuditEvent event) {
237 boolean accepted = true;
238
239 if (event.violation() != null) {
240 // Lazy update. If the first event for the current file, update file
241 // contents and tag suppressions
242 final FileContents currentContents = event.fileContents();
243 fileContentsHolder.lazyUpdate(currentContents, this::tagSuppressions);
244 final Tag matchTag = findNearestMatch(event);
245 accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
246 }
247 return accepted;
248 }
249
250 /**
251 * Finds the nearest comment text tag that matches an audit event.
252 * The nearest tag is before the line and column of the event.
253 *
254 * @param event the {@code TreeWalkerAuditEvent} to match.
255 * @return The {@code Tag} nearest event.
256 */
257 private Tag findNearestMatch(TreeWalkerAuditEvent event) {
258 Tag result = null;
259 for (Tag tag : tags) {
260 final int eventLine = event.getLine();
261 if (tag.getLine() > eventLine
262 || tag.getLine() == eventLine
263 && tag.getColumn() > event.getColumn()) {
264 break;
265 }
266 if (tag.isMatch(event)) {
267 result = tag;
268 }
269 }
270 return result;
271 }
272
273 /**
274 * Collects all the suppression tags for all comments into a list and
275 * sorts the list.
276 */
277 private void tagSuppressions() {
278 tags.clear();
279 final FileContents contents = fileContentsHolder.get();
280 if (checkCPP) {
281 tagSuppressions(contents.getSingleLineComments().values());
282 }
283 if (checkC) {
284 final Collection<List<TextBlock>> cComments = contents
285 .getBlockComments().values();
286 cComments.forEach(this::tagSuppressions);
287 }
288 tags.sort(Comparator.naturalOrder());
289 }
290
291 /**
292 * Appends the suppressions in a collection of comments to the full
293 * set of suppression tags.
294 *
295 * @param comments the set of comments.
296 */
297 private void tagSuppressions(Collection<TextBlock> comments) {
298 for (TextBlock comment : comments) {
299 final int startLineNo = comment.getStartLineNo();
300 final String[] text = comment.getText();
301 tagCommentLine(text[0], startLineNo, comment.getStartColNo());
302 for (int i = 1; i < text.length; i++) {
303 tagCommentLine(text[i], startLineNo + i, 0);
304 }
305 }
306 }
307
308 /**
309 * Tags a string if it matches the format for turning
310 * checkstyle reporting on or the format for turning reporting off.
311 *
312 * @param text the string to tag.
313 * @param line the line number of text.
314 * @param column the column number of text.
315 */
316 private void tagCommentLine(String text, int line, int column) {
317 final Matcher offMatcher = offCommentFormat.matcher(text);
318 if (offMatcher.find()) {
319 addTag(offMatcher.group(0), line, column, TagType.OFF);
320 }
321 else {
322 final Matcher onMatcher = onCommentFormat.matcher(text);
323 if (onMatcher.find()) {
324 addTag(onMatcher.group(0), line, column, TagType.ON);
325 }
326 }
327 }
328
329 /**
330 * Adds a {@code Tag} to the list of all tags.
331 *
332 * @param text the text of the tag.
333 * @param line the line number of the tag.
334 * @param column the column number of the tag.
335 * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
336 */
337 private void addTag(String text, int line, int column, TagType reportingOn) {
338 final Tag tag = new Tag(line, column, text, reportingOn, this);
339 tags.add(tag);
340 }
341
342 /**
343 * A Tag holds a suppression comment and its location, and determines
344 * whether the suppression turns checkstyle reporting on or off.
345 */
346 private static final class Tag
347 implements Comparable<Tag> {
348
349 /** The text of the tag. */
350 private final String text;
351
352 /** The line number of the tag. */
353 private final int line;
354
355 /** The column number of the tag. */
356 private final int column;
357
358 /** Determines whether the suppression turns checkstyle reporting on. */
359 private final TagType tagType;
360
361 /** The parsed check regexp, expanded for the text of this tag. */
362 private final Pattern tagCheckRegexp;
363
364 /** The parsed message regexp, expanded for the text of this tag. */
365 private final Pattern tagMessageRegexp;
366
367 /** The parsed check ID regexp, expanded for the text of this tag. */
368 private final Pattern tagIdRegexp;
369
370 /**
371 * Constructs a tag.
372 *
373 * @param line the line number.
374 * @param column the column number.
375 * @param text the text of the suppression.
376 * @param tagType {@code ON} if the tag turns checkstyle reporting.
377 * @param filter the {@code SuppressionCommentFilter} with the context
378 * @throws IllegalArgumentException if unable to parse expanded text.
379 */
380 private Tag(int line, int column, String text, TagType tagType,
381 SuppressionCommentFilter filter) {
382 this.line = line;
383 this.column = column;
384 this.text = text;
385 this.tagType = tagType;
386
387 final Pattern commentFormat;
388 if (this.tagType == TagType.ON) {
389 commentFormat = filter.onCommentFormat;
390 }
391 else {
392 commentFormat = filter.offCommentFormat;
393 }
394
395 // Expand regexp for check and message
396 // Does not intern Patterns with Utils.getPattern()
397 String format = "";
398 try {
399 format = CommonUtil.fillTemplateWithStringsByRegexp(
400 filter.checkFormat, text, commentFormat);
401 tagCheckRegexp = Pattern.compile(format);
402
403 if (filter.messageFormat == null) {
404 tagMessageRegexp = null;
405 }
406 else {
407 format = CommonUtil.fillTemplateWithStringsByRegexp(
408 filter.messageFormat, text, commentFormat);
409 tagMessageRegexp = Pattern.compile(format);
410 }
411
412 if (filter.idFormat == null) {
413 tagIdRegexp = null;
414 }
415 else {
416 format = CommonUtil.fillTemplateWithStringsByRegexp(
417 filter.idFormat, text, commentFormat);
418 tagIdRegexp = Pattern.compile(format);
419 }
420 }
421 catch (final PatternSyntaxException exc) {
422 throw new IllegalArgumentException(
423 "unable to parse expanded comment " + format, exc);
424 }
425 }
426
427 /**
428 * Returns line number of the tag in the source file.
429 *
430 * @return the line number of the tag in the source file.
431 */
432 /* package */ int getLine() {
433 return line;
434 }
435
436 /**
437 * Determines the column number of the tag in the source file.
438 * Will be 0 for all lines of multiline comment, except the
439 * first line.
440 *
441 * @return the column number of the tag in the source file.
442 */
443 /* package */ int getColumn() {
444 return column;
445 }
446
447 /**
448 * Determines whether the suppression turns checkstyle reporting on or
449 * off.
450 *
451 * @return {@code ON} if the suppression turns reporting on.
452 */
453 /* package */ TagType getTagType() {
454 return tagType;
455 }
456
457 /**
458 * Compares the position of this tag in the file
459 * with the position of another tag.
460 *
461 * @param object the tag to compare with this one.
462 * @return a negative number if this tag is before the other tag,
463 * 0 if they are at the same position, and a positive number if this
464 * tag is after the other tag.
465 */
466 @Override
467 public int compareTo(Tag object) {
468 final int result;
469 if (line == object.line) {
470 result = Integer.compare(column, object.column);
471 }
472 else {
473 result = Integer.compare(line, object.line);
474 }
475 return result;
476 }
477
478 /**
479 * Indicates whether some other object is "equal to" this one.
480 * Suppression on enumeration is needed so code stays consistent.
481 *
482 * @noinspection EqualsCalledOnEnumConstant
483 * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
484 * code consistent
485 */
486 @Override
487 public boolean equals(Object other) {
488 if (this == other) {
489 return true;
490 }
491 if (other == null || getClass() != other.getClass()) {
492 return false;
493 }
494 final Tag tag = (Tag) other;
495 return line == tag.line
496 && column == tag.column
497 && Objects.equals(tagType, tag.tagType)
498 && Objects.equals(text, tag.text)
499 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
500 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
501 && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
502 }
503
504 @Override
505 public int hashCode() {
506 return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
507 tagIdRegexp);
508 }
509
510 /**
511 * Determines whether the source of an audit event
512 * matches the text of this tag.
513 *
514 * @param event the {@code TreeWalkerAuditEvent} to check.
515 * @return true if the source of event matches the text of this tag.
516 */
517 /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
518 return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
519 }
520
521 /**
522 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
523 *
524 * @param event {@code TreeWalkerAuditEvent} instance.
525 * @return true if the {@code TreeWalkerAuditEvent} source name matches the check format.
526 */
527 private boolean isCheckMatch(TreeWalkerAuditEvent event) {
528 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
529 return checkMatcher.find();
530 }
531
532 /**
533 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
534 *
535 * @param event {@code TreeWalkerAuditEvent} instance.
536 * @return true if the {@code TreeWalkerAuditEvent} module ID matches the ID format.
537 */
538 private boolean isIdMatch(TreeWalkerAuditEvent event) {
539 boolean match = true;
540 if (tagIdRegexp != null) {
541 if (event.getModuleId() == null) {
542 match = false;
543 }
544 else {
545 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
546 match = idMatcher.find();
547 }
548 }
549 return match;
550 }
551
552 /**
553 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
554 *
555 * @param event {@code TreeWalkerAuditEvent} instance.
556 * @return true if the {@code TreeWalkerAuditEvent} message matches the message format.
557 */
558 private boolean isMessageMatch(TreeWalkerAuditEvent event) {
559 boolean match = true;
560 if (tagMessageRegexp != null) {
561 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
562 match = messageMatcher.find();
563 }
564 return match;
565 }
566
567 @Override
568 public String toString() {
569 return "Tag[text='" + text + '\''
570 + ", line=" + line
571 + ", column=" + column
572 + ", type=" + tagType
573 + ", tagCheckRegexp=" + tagCheckRegexp
574 + ", tagMessageRegexp=" + tagMessageRegexp
575 + ", tagIdRegexp=" + tagIdRegexp + ']';
576 }
577
578 }
579
580 }