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