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