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.Collections;
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 * Setter to specify comment pattern to trigger filter to begin suppression.
152 *
153 * @param pattern a pattern.
154 * @since 3.5
155 */
156 public final void setOffCommentFormat(Pattern pattern) {
157 offCommentFormat = pattern;
158 }
159
160 /**
161 * Setter to specify comment pattern to trigger filter to end suppression.
162 *
163 * @param pattern a pattern.
164 * @since 3.5
165 */
166 public final void setOnCommentFormat(Pattern pattern) {
167 onCommentFormat = pattern;
168 }
169
170 /**
171 * Setter to specify check pattern to suppress.
172 * The pattern is matched against the fully qualified class name of the Check.
173 *
174 * @param format a {@code String} value
175 * @since 3.5
176 */
177 public final void setCheckFormat(String format) {
178 checkFormat = format;
179 }
180
181 /**
182 * Setter to specify message pattern to suppress.
183 *
184 * @param format a {@code String} value
185 * @since 3.5
186 */
187 public void setMessageFormat(String format) {
188 messageFormat = format;
189 }
190
191 /**
192 * Setter to specify check ID pattern to suppress.
193 *
194 * @param format a {@code String} value
195 * @since 8.24
196 */
197 public void setIdFormat(String format) {
198 idFormat = format;
199 }
200
201 /**
202 * Setter to control whether to check C++ style comments ({@code //}).
203 *
204 * @param checkCppComments {@code true} if C++ comments are checked.
205 * @since 3.5
206 */
207 // -@cs[AbbreviationAsWordInName] We can not change it as,
208 // check's property is a part of API (used in configurations).
209 public void setCheckCPP(boolean checkCppComments) {
210 checkCPP = checkCppComments;
211 }
212
213 /**
214 * Setter to control whether to check C style comments (/* ... */).
215 *
216 * @param checkC {@code true} if C comments are checked.
217 * @since 3.5
218 */
219 public void setCheckC(boolean checkC) {
220 this.checkC = checkC;
221 }
222
223 @Override
224 protected void finishLocalSetup() {
225 // No code by default
226 }
227
228 @Override
229 public boolean accept(TreeWalkerAuditEvent event) {
230 boolean accepted = true;
231
232 if (event.violation() != null) {
233 // Lazy update. If the first event for the current file, update file
234 // contents and tag suppressions
235 final FileContents currentContents = event.fileContents();
236 fileContentsHolder.lazyUpdate(currentContents, this::tagSuppressions);
237 final Tag matchTag = findNearestMatch(event);
238 accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
239 }
240 return accepted;
241 }
242
243 /**
244 * Finds the nearest comment text tag that matches an audit event.
245 * The nearest tag is before the line and column of the event.
246 *
247 * @param event the {@code TreeWalkerAuditEvent} to match.
248 * @return The {@code Tag} nearest event.
249 */
250 private Tag findNearestMatch(TreeWalkerAuditEvent event) {
251 Tag result = null;
252 for (Tag tag : tags) {
253 final int eventLine = event.getLine();
254 if (tag.getLine() > eventLine
255 || tag.getLine() == eventLine
256 && tag.getColumn() > event.getColumn()) {
257 break;
258 }
259 if (tag.isMatch(event)) {
260 result = tag;
261 }
262 }
263 return result;
264 }
265
266 /**
267 * Collects all the suppression tags for all comments into a list and
268 * sorts the list.
269 */
270 private void tagSuppressions() {
271 tags.clear();
272 final FileContents contents = fileContentsHolder.get();
273 if (checkCPP) {
274 tagSuppressions(contents.getSingleLineComments().values());
275 }
276 if (checkC) {
277 final Collection<List<TextBlock>> cComments = contents
278 .getBlockComments().values();
279 cComments.forEach(this::tagSuppressions);
280 }
281 Collections.sort(tags);
282 }
283
284 /**
285 * Appends the suppressions in a collection of comments to the full
286 * set of suppression tags.
287 *
288 * @param comments the set of comments.
289 */
290 private void tagSuppressions(Collection<TextBlock> comments) {
291 for (TextBlock comment : comments) {
292 final int startLineNo = comment.getStartLineNo();
293 final String[] text = comment.getText();
294 tagCommentLine(text[0], startLineNo, comment.getStartColNo());
295 for (int i = 1; i < text.length; i++) {
296 tagCommentLine(text[i], startLineNo + i, 0);
297 }
298 }
299 }
300
301 /**
302 * Tags a string if it matches the format for turning
303 * checkstyle reporting on or the format for turning reporting off.
304 *
305 * @param text the string to tag.
306 * @param line the line number of text.
307 * @param column the column number of text.
308 */
309 private void tagCommentLine(String text, int line, int column) {
310 final Matcher offMatcher = offCommentFormat.matcher(text);
311 if (offMatcher.find()) {
312 addTag(offMatcher.group(0), line, column, TagType.OFF);
313 }
314 else {
315 final Matcher onMatcher = onCommentFormat.matcher(text);
316 if (onMatcher.find()) {
317 addTag(onMatcher.group(0), line, column, TagType.ON);
318 }
319 }
320 }
321
322 /**
323 * Adds a {@code Tag} to the list of all tags.
324 *
325 * @param text the text of the tag.
326 * @param line the line number of the tag.
327 * @param column the column number of the tag.
328 * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
329 */
330 private void addTag(String text, int line, int column, TagType reportingOn) {
331 final Tag tag = new Tag(line, column, text, reportingOn, this);
332 tags.add(tag);
333 }
334
335 /**
336 * A Tag holds a suppression comment and its location, and determines
337 * whether the suppression turns checkstyle reporting on or off.
338 */
339 private static final class Tag
340 implements Comparable<Tag> {
341
342 /** The text of the tag. */
343 private final String text;
344
345 /** The line number of the tag. */
346 private final int line;
347
348 /** The column number of the tag. */
349 private final int column;
350
351 /** Determines whether the suppression turns checkstyle reporting on. */
352 private final TagType tagType;
353
354 /** The parsed check regexp, expanded for the text of this tag. */
355 private final Pattern tagCheckRegexp;
356
357 /** The parsed message regexp, expanded for the text of this tag. */
358 private final Pattern tagMessageRegexp;
359
360 /** The parsed check ID regexp, expanded for the text of this tag. */
361 private final Pattern tagIdRegexp;
362
363 /**
364 * Constructs a tag.
365 *
366 * @param line the line number.
367 * @param column the column number.
368 * @param text the text of the suppression.
369 * @param tagType {@code ON} if the tag turns checkstyle reporting.
370 * @param filter the {@code SuppressionCommentFilter} with the context
371 * @throws IllegalArgumentException if unable to parse expanded text.
372 */
373 private Tag(int line, int column, String text, TagType tagType,
374 SuppressionCommentFilter filter) {
375 this.line = line;
376 this.column = column;
377 this.text = text;
378 this.tagType = tagType;
379
380 final Pattern commentFormat;
381 if (this.tagType == TagType.ON) {
382 commentFormat = filter.onCommentFormat;
383 }
384 else {
385 commentFormat = filter.offCommentFormat;
386 }
387
388 // Expand regexp for check and message
389 // Does not intern Patterns with Utils.getPattern()
390 String format = "";
391 try {
392 format = CommonUtil.fillTemplateWithStringsByRegexp(
393 filter.checkFormat, text, commentFormat);
394 tagCheckRegexp = Pattern.compile(format);
395
396 if (filter.messageFormat == null) {
397 tagMessageRegexp = null;
398 }
399 else {
400 format = CommonUtil.fillTemplateWithStringsByRegexp(
401 filter.messageFormat, text, commentFormat);
402 tagMessageRegexp = Pattern.compile(format);
403 }
404
405 if (filter.idFormat == null) {
406 tagIdRegexp = null;
407 }
408 else {
409 format = CommonUtil.fillTemplateWithStringsByRegexp(
410 filter.idFormat, text, commentFormat);
411 tagIdRegexp = Pattern.compile(format);
412 }
413 }
414 catch (final PatternSyntaxException exc) {
415 throw new IllegalArgumentException(
416 "unable to parse expanded comment " + format, exc);
417 }
418 }
419
420 /**
421 * Returns line number of the tag in the source file.
422 *
423 * @return the line number of the tag in the source file.
424 */
425 /* package */ int getLine() {
426 return line;
427 }
428
429 /**
430 * Determines the column number of the tag in the source file.
431 * Will be 0 for all lines of multiline comment, except the
432 * first line.
433 *
434 * @return the column number of the tag in the source file.
435 */
436 /* package */ int getColumn() {
437 return column;
438 }
439
440 /**
441 * Determines whether the suppression turns checkstyle reporting on or
442 * off.
443 *
444 * @return {@code ON} if the suppression turns reporting on.
445 */
446 /* package */ TagType getTagType() {
447 return tagType;
448 }
449
450 /**
451 * Compares the position of this tag in the file
452 * with the position of another tag.
453 *
454 * @param object the tag to compare with this one.
455 * @return a negative number if this tag is before the other tag,
456 * 0 if they are at the same position, and a positive number if this
457 * tag is after the other tag.
458 */
459 @Override
460 public int compareTo(Tag object) {
461 final int result;
462 if (line == object.line) {
463 result = Integer.compare(column, object.column);
464 }
465 else {
466 result = Integer.compare(line, object.line);
467 }
468 return result;
469 }
470
471 /**
472 * Indicates whether some other object is "equal to" this one.
473 * Suppression on enumeration is needed so code stays consistent.
474 *
475 * @noinspection EqualsCalledOnEnumConstant
476 * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
477 * code consistent
478 */
479 @Override
480 public boolean equals(Object other) {
481 if (this == other) {
482 return true;
483 }
484 if (other == null || getClass() != other.getClass()) {
485 return false;
486 }
487 final Tag tag = (Tag) other;
488 return line == tag.line
489 && column == tag.column
490 && Objects.equals(tagType, tag.tagType)
491 && Objects.equals(text, tag.text)
492 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
493 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
494 && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
495 }
496
497 @Override
498 public int hashCode() {
499 return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
500 tagIdRegexp);
501 }
502
503 /**
504 * Determines whether the source of an audit event
505 * matches the text of this tag.
506 *
507 * @param event the {@code TreeWalkerAuditEvent} to check.
508 * @return true if the source of event matches the text of this tag.
509 */
510 /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
511 return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
512 }
513
514 /**
515 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
516 *
517 * @param event {@link TreeWalkerAuditEvent} instance.
518 * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
519 */
520 private boolean isCheckMatch(TreeWalkerAuditEvent event) {
521 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
522 return checkMatcher.find();
523 }
524
525 /**
526 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
527 *
528 * @param event {@link TreeWalkerAuditEvent} instance.
529 * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
530 */
531 private boolean isIdMatch(TreeWalkerAuditEvent event) {
532 boolean match = true;
533 if (tagIdRegexp != null) {
534 if (event.getModuleId() == null) {
535 match = false;
536 }
537 else {
538 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
539 match = idMatcher.find();
540 }
541 }
542 return match;
543 }
544
545 /**
546 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
547 *
548 * @param event {@link TreeWalkerAuditEvent} instance.
549 * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
550 */
551 private boolean isMessageMatch(TreeWalkerAuditEvent event) {
552 boolean match = true;
553 if (tagMessageRegexp != null) {
554 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
555 match = messageMatcher.find();
556 }
557 return match;
558 }
559
560 @Override
561 public String toString() {
562 return "Tag[text='" + text + '\''
563 + ", line=" + line
564 + ", column=" + column
565 + ", type=" + tagType
566 + ", tagCheckRegexp=" + tagCheckRegexp
567 + ", tagMessageRegexp=" + tagMessageRegexp
568 + ", tagIdRegexp=" + tagIdRegexp + ']';
569 }
570
571 }
572
573 }