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