001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.filters; 021 022import java.lang.ref.WeakReference; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.List; 027import java.util.Objects; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030import java.util.regex.PatternSyntaxException; 031 032import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean; 033import com.puppycrawl.tools.checkstyle.PropertyType; 034import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent; 035import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 036import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 037import com.puppycrawl.tools.checkstyle.api.FileContents; 038import com.puppycrawl.tools.checkstyle.api.TextBlock; 039import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 040 041/** 042 * <div> 043 * Filter {@code SuppressionCommentFilter} uses pairs of comments to suppress audit events. 044 * </div> 045 * 046 * <p> 047 * Rationale: 048 * Sometimes there are legitimate reasons for violating a check. When 049 * this is a matter of the code in question and not personal 050 * preference, the best place to override the policy is in the code 051 * itself. Semi-structured comments can be associated with the check. 052 * This is sometimes superior to a separate suppressions file, which 053 * must be kept up-to-date as the source file is edited. 054 * </p> 055 * 056 * <p> 057 * Note that the suppression comment should be put before the violation. 058 * You can use more than one suppression comment each on separate line. 059 * </p> 060 * 061 * <p> 062 * Attention: This filter may only be specified within the TreeWalker module 063 * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also 064 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, a 065 * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html"> 066 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used. 067 * </p> 068 * 069 * <p> 070 * Notes: 071 * {@code offCommentFormat} and {@code onCommentFormat} must have equal 072 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Matcher.html#groupCount()"> 073 * paren counts</a>. 074 * </p> 075 * 076 * <p> 077 * SuppressionCommentFilter can suppress Checks that have Treewalker as parent module. 078 * </p> 079 * 080 * @since 3.5 081 */ 082public class SuppressionCommentFilter 083 extends AbstractAutomaticBean 084 implements TreeWalkerFilter { 085 086 /** 087 * Enum to be used for switching checkstyle reporting for tags. 088 */ 089 public enum TagType { 090 091 /** 092 * Switch reporting on. 093 */ 094 ON, 095 /** 096 * Switch reporting off. 097 */ 098 OFF, 099 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}