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