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