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