001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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.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.TreeWalkerAuditEvent; 032import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 033import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 034import com.puppycrawl.tools.checkstyle.api.FileContents; 035import com.puppycrawl.tools.checkstyle.api.TextBlock; 036import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 037 038/** 039 * <p> 040 * Filter {@code SuppressWithNearbyCommentFilter} uses nearby comments to suppress audit events. 041 * </p> 042 * <p> 043 * Rationale: Same as {@code SuppressionCommentFilter}. 044 * Whereas the SuppressionCommentFilter uses matched pairs of filters to turn 045 * on/off comment matching, {@code SuppressWithNearbyCommentFilter} uses single comments. 046 * This requires fewer lines to mark a region, and may be aesthetically preferable in some contexts. 047 * </p> 048 * <p> 049 * Attention: This filter may only be specified within the TreeWalker module 050 * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also 051 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, 052 * a <a href="https://checkstyle.org/config_filters.html#SuppressWithPlainTextCommentFilter"> 053 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used. 054 * </p> 055 * <ul> 056 * <li> 057 * Property {@code commentFormat} - Specify comment pattern to trigger filter to begin suppression. 058 * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}. 059 * </li> 060 * <li> 061 * Property {@code checkFormat} - Specify check pattern to suppress. 062 * Default value is {@code ".*"}. 063 * </li> 064 * <li> 065 * Property {@code messageFormat} - Define message pattern to suppress. 066 * Default value is {@code null}. 067 * </li> 068 * <li> 069 * Property {@code idFormat} - Specify check ID pattern to suppress. 070 * Default value is {@code null}. 071 * </li> 072 * <li> 073 * Property {@code influenceFormat} - Specify negative/zero/positive value that 074 * defines the number of lines preceding/at/following the suppression comment. 075 * Default value is {@code "0"}. 076 * </li> 077 * <li> 078 * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}). 079 * Default value is {@code true}. 080 * </li> 081 * <li> 082 * Property {@code checkC} - Control whether to check C style comments ({@code /* ... */}). 083 * Default value is {@code true}. 084 * </li> 085 * </ul> 086 * <p> 087 * To configure a filter to suppress audit events for <i>check</i> on any line 088 * with a comment {@code SUPPRESS CHECKSTYLE <i>check</i>}: 089 * </p> 090 * <pre> 091 * <module name="SuppressWithNearbyCommentFilter"/> 092 * </pre> 093 * <pre> 094 * private int [] array; // SUPPRESS CHECKSTYLE 095 * </pre> 096 * <p> 097 * To configure a filter to suppress all audit events on any line containing 098 * the comment {@code CHECKSTYLE IGNORE THIS LINE}: 099 * </p> 100 * <pre> 101 * <module name="SuppressWithNearbyCommentFilter"> 102 * <property name="commentFormat" value="CHECKSTYLE IGNORE THIS LINE"/> 103 * <property name="checkFormat" value=".*"/> 104 * <property name="influenceFormat" value="0"/> 105 * </module> 106 * </pre> 107 * <pre> 108 * public static final int lowerCaseConstant; // CHECKSTYLE IGNORE THIS LINE 109 * </pre> 110 * <p> 111 * To configure a filter so that {@code // OK to catch (Throwable|Exception|RuntimeException) here} 112 * permits the current and previous line to avoid generating an IllegalCatch audit event: 113 * </p> 114 * <pre> 115 * <module name="SuppressWithNearbyCommentFilter"> 116 * <property name="commentFormat" value="OK to catch (\w+) here"/> 117 * <property name="checkFormat" value="IllegalCatchCheck"/> 118 * <property name="messageFormat" value="$1"/> 119 * <property name="influenceFormat" value="-1"/> 120 * </module> 121 * </pre> 122 * <pre> 123 * . . . 124 * catch (RuntimeException re) { 125 * // OK to catch RuntimeException here 126 * } 127 * catch (Throwable th) { ... } 128 * . . . 129 * </pre> 130 * <p> 131 * To configure a filter so that {@code CHECKSTYLE IGNORE <i>check</i> FOR NEXT 132 * <i>var</i> LINES} avoids triggering any audits for the given check for 133 * the current line and the next <i>var</i> lines (for a total of <i>var</i>+1 lines): 134 * </p> 135 * <pre> 136 * <module name="SuppressWithNearbyCommentFilter"> 137 * <property name="commentFormat" 138 * value="CHECKSTYLE IGNORE (\w+) FOR NEXT (\d+) LINES"/> 139 * <property name="checkFormat" value="$1"/> 140 * <property name="influenceFormat" value="$2"/> 141 * </module> 142 * </pre> 143 * <pre> 144 * static final int lowerCaseConstant; // CHECKSTYLE IGNORE ConstantNameCheck FOR NEXT 3 LINES 145 * static final int lowerCaseConstant1; 146 * static final int lowerCaseConstant2; 147 * static final int lowerCaseConstant3; 148 * static final int lowerCaseConstant4; // will warn here 149 * </pre> 150 * <p> 151 * To configure a filter to avoid any audits on code like: 152 * </p> 153 * <pre> 154 * <module name="SuppressWithNearbyCommentFilter"> 155 * <property name="commentFormat" 156 * value="ALLOW (\\w+) ON PREVIOUS LINE"/> 157 * <property name="checkFormat" value="$1"/> 158 * <property name="influenceFormat" value="-1"/> 159 * </module> 160 * </pre> 161 * <pre> 162 * private int D2; 163 * // ALLOW MemberName ON PREVIOUS LINE 164 * . . . 165 * </pre> 166 * <p> 167 * To configure a filter to allow suppress one or more Checks (separated by "|") 168 * and demand comment no less than 14 symbols: 169 * </p> 170 * <pre> 171 * <module name="SuppressWithNearbyCommentFilter"> 172 * <property name="commentFormat" 173 * value="@cs\.suppress \[(\w+(\|\w+)*)\] \w[-\.'`,:;\w ]{14,}"/> 174 * <property name="checkFormat" value="$1"/> 175 * <property name="influenceFormat" value="1"/> 176 * </module> 177 * </pre> 178 * <pre> 179 * public static final int [] array; // @cs.suppress [ConstantName|NoWhitespaceAfter] A comment here 180 * </pre> 181 * <p> 182 * It is possible to specify an ID of checks, so that it can be leveraged by 183 * the SuppressWithNearbyCommentFilter to skip validations. The following examples show how to skip 184 * validations near code that has comment like {@code // @cs-: <ID/> (reason)}, 185 * where ID is the ID of checks you want to suppress. 186 * </p> 187 * <p> 188 * Examples of Checkstyle checks configuration: 189 * </p> 190 * <pre> 191 * <module name="RegexpSinglelineJava"> 192 * <property name="id" value="ignore"/> 193 * <property name="format" value="^.*@Ignore\s*$"/> 194 * <property name="message" value="@Ignore should have a reason."/> 195 * </module> 196 * 197 * <module name="RegexpSinglelineJava"> 198 * <property name="id" value="systemout"/> 199 * <property name="format" value="^.*System\.(out|err).*$"/> 200 * <property name="message" value="Don't use System.out/err, use SLF4J instead."/> 201 * </module> 202 * </pre> 203 * <p> 204 * Example of SuppressWithNearbyCommentFilter configuration (idFormat which is set to 205 * '$1' points that ID of the checks is in the first group of commentFormat regular expressions): 206 * </p> 207 * <pre> 208 * <module name="SuppressWithNearbyCommentFilter"> 209 * <property name="commentFormat" value="@cs-: (\w+) \(.*\)"/> 210 * <property name="idFormat" value="$1"/> 211 * <property name="influenceFormat" value="0"/> 212 * </module> 213 * </pre> 214 * <pre> 215 * @Ignore // @cs-: ignore (test has not been implemented yet) 216 * @Test 217 * public void testMethod() { } 218 * 219 * public static void foo() { 220 * System.out.println("Debug info."); // @cs-: systemout (should not fail RegexpSinglelineJava) 221 * } 222 * </pre> 223 * <p> 224 * Example of how to configure the check to suppress more than one checks. 225 * The influence format format is specified in the second regexp group. 226 * </p> 227 * <pre> 228 * <module name="SuppressWithNearbyCommentFilter"> 229 * <property name="commentFormat" value="@cs-\: ([\w\|]+) influence (\d+)"/> 230 * <property name="checkFormat" value="$1"/> 231 * <property name="influenceFormat" value="$2"/> 232 * </module> 233 * </pre> 234 * <pre> 235 * // @cs-: ClassDataAbstractionCoupling influence 2 236 * // @cs-: MagicNumber influence 4 237 * @Service // no violations from ClassDataAbstractionCoupling here 238 * @Transactional 239 * public class UserService { 240 * private int value = 10022; // no violations from MagicNumber here 241 * } 242 * </pre> 243 * 244 * @since 5.0 245 */ 246public class SuppressWithNearbyCommentFilter 247 extends AutomaticBean 248 implements TreeWalkerFilter { 249 250 /** Format to turns checkstyle reporting off. */ 251 private static final String DEFAULT_COMMENT_FORMAT = 252 "SUPPRESS CHECKSTYLE (\\w+)"; 253 254 /** Default regex for checks that should be suppressed. */ 255 private static final String DEFAULT_CHECK_FORMAT = ".*"; 256 257 /** Default regex for lines that should be suppressed. */ 258 private static final String DEFAULT_INFLUENCE_FORMAT = "0"; 259 260 /** Tagged comments. */ 261 private final List<Tag> tags = new ArrayList<>(); 262 263 /** Control whether to check C style comments ({@code /* ... */}). */ 264 private boolean checkC = true; 265 266 /** Control whether to check C++ style comments ({@code //}). */ 267 // -@cs[AbbreviationAsWordInName] We can not change it as, 268 // check's property is a part of API (used in configurations). 269 private boolean checkCPP = true; 270 271 /** Specify comment pattern to trigger filter to begin suppression. */ 272 private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT); 273 274 /** Specify check pattern to suppress. */ 275 private String checkFormat = DEFAULT_CHECK_FORMAT; 276 277 /** Define message pattern to suppress. */ 278 private String messageFormat; 279 280 /** Specify check ID pattern to suppress. */ 281 private String idFormat; 282 283 /** 284 * Specify negative/zero/positive value that defines the number of lines 285 * preceding/at/following the suppression comment. 286 */ 287 private String influenceFormat = DEFAULT_INFLUENCE_FORMAT; 288 289 /** 290 * References the current FileContents for this filter. 291 * Since this is a weak reference to the FileContents, the FileContents 292 * can be reclaimed as soon as the strong references in TreeWalker 293 * are reassigned to the next FileContents, at which time filtering for 294 * the current FileContents is finished. 295 */ 296 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); 297 298 /** 299 * Setter to specify comment pattern to trigger filter to begin suppression. 300 * @param pattern a pattern. 301 */ 302 public final void setCommentFormat(Pattern pattern) { 303 commentFormat = pattern; 304 } 305 306 /** 307 * Returns FileContents for this filter. 308 * @return the FileContents for this filter. 309 */ 310 private FileContents getFileContents() { 311 return fileContentsReference.get(); 312 } 313 314 /** 315 * Set the FileContents for this filter. 316 * @param fileContents the FileContents for this filter. 317 * @noinspection WeakerAccess 318 */ 319 public void setFileContents(FileContents fileContents) { 320 fileContentsReference = new WeakReference<>(fileContents); 321 } 322 323 /** 324 * Setter to specify check pattern to suppress. 325 * @param format a {@code String} value 326 */ 327 public final void setCheckFormat(String format) { 328 checkFormat = format; 329 } 330 331 /** 332 * Setter to define message pattern to suppress. 333 * @param format a {@code String} value 334 */ 335 public void setMessageFormat(String format) { 336 messageFormat = format; 337 } 338 339 /** 340 * Setter to specify check ID pattern to suppress. 341 * @param format a {@code String} value 342 */ 343 public void setIdFormat(String format) { 344 idFormat = format; 345 } 346 347 /** 348 * Setter to specify negative/zero/positive value that defines the number 349 * of lines preceding/at/following the suppression comment. 350 * @param format a {@code String} value 351 */ 352 public final void setInfluenceFormat(String format) { 353 influenceFormat = format; 354 } 355 356 /** 357 * Setter to control whether to check C++ style comments ({@code //}). 358 * @param checkCpp {@code true} if C++ comments are checked. 359 */ 360 // -@cs[AbbreviationAsWordInName] We can not change it as, 361 // check's property is a part of API (used in configurations). 362 public void setCheckCPP(boolean checkCpp) { 363 checkCPP = checkCpp; 364 } 365 366 /** 367 * Setter to control whether to check C style comments ({@code /* ... */}). 368 * @param checkC {@code true} if C comments are checked. 369 */ 370 public void setCheckC(boolean checkC) { 371 this.checkC = checkC; 372 } 373 374 @Override 375 protected void finishLocalSetup() { 376 // No code by default 377 } 378 379 @Override 380 public boolean accept(TreeWalkerAuditEvent event) { 381 boolean accepted = true; 382 383 if (event.getLocalizedMessage() != null) { 384 // Lazy update. If the first event for the current file, update file 385 // contents and tag suppressions 386 final FileContents currentContents = event.getFileContents(); 387 388 if (getFileContents() != currentContents) { 389 setFileContents(currentContents); 390 tagSuppressions(); 391 } 392 if (matchesTag(event)) { 393 accepted = false; 394 } 395 } 396 return accepted; 397 } 398 399 /** 400 * Whether current event matches any tag from {@link #tags}. 401 * @param event TreeWalkerAuditEvent to test match on {@link #tags}. 402 * @return true if event matches any tag from {@link #tags}, false otherwise. 403 */ 404 private boolean matchesTag(TreeWalkerAuditEvent event) { 405 boolean result = false; 406 for (final Tag tag : tags) { 407 if (tag.isMatch(event)) { 408 result = true; 409 break; 410 } 411 } 412 return result; 413 } 414 415 /** 416 * Collects all the suppression tags for all comments into a list and 417 * sorts the list. 418 */ 419 private void tagSuppressions() { 420 tags.clear(); 421 final FileContents contents = getFileContents(); 422 if (checkCPP) { 423 tagSuppressions(contents.getSingleLineComments().values()); 424 } 425 if (checkC) { 426 final Collection<List<TextBlock>> cComments = 427 contents.getBlockComments().values(); 428 cComments.forEach(this::tagSuppressions); 429 } 430 } 431 432 /** 433 * Appends the suppressions in a collection of comments to the full 434 * set of suppression tags. 435 * @param comments the set of comments. 436 */ 437 private void tagSuppressions(Collection<TextBlock> comments) { 438 for (final TextBlock comment : comments) { 439 final int startLineNo = comment.getStartLineNo(); 440 final String[] text = comment.getText(); 441 tagCommentLine(text[0], startLineNo); 442 for (int i = 1; i < text.length; i++) { 443 tagCommentLine(text[i], startLineNo + i); 444 } 445 } 446 } 447 448 /** 449 * Tags a string if it matches the format for turning 450 * checkstyle reporting on or the format for turning reporting off. 451 * @param text the string to tag. 452 * @param line the line number of text. 453 */ 454 private void tagCommentLine(String text, int line) { 455 final Matcher matcher = commentFormat.matcher(text); 456 if (matcher.find()) { 457 addTag(matcher.group(0), line); 458 } 459 } 460 461 /** 462 * Adds a comment suppression {@code Tag} to the list of all tags. 463 * @param text the text of the tag. 464 * @param line the line number of the tag. 465 */ 466 private void addTag(String text, int line) { 467 final Tag tag = new Tag(text, line, this); 468 tags.add(tag); 469 } 470 471 /** 472 * A Tag holds a suppression comment and its location. 473 */ 474 private static final class Tag { 475 476 /** The text of the tag. */ 477 private final String text; 478 479 /** The first line where warnings may be suppressed. */ 480 private final int firstLine; 481 482 /** The last line where warnings may be suppressed. */ 483 private final int lastLine; 484 485 /** The parsed check regexp, expanded for the text of this tag. */ 486 private final Pattern tagCheckRegexp; 487 488 /** The parsed message regexp, expanded for the text of this tag. */ 489 private final Pattern tagMessageRegexp; 490 491 /** The parsed check ID regexp, expanded for the text of this tag. */ 492 private final Pattern tagIdRegexp; 493 494 /** 495 * Constructs a tag. 496 * @param text the text of the suppression. 497 * @param line the line number. 498 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context 499 * @throws IllegalArgumentException if unable to parse expanded text. 500 */ 501 /* package */ Tag(String text, int line, SuppressWithNearbyCommentFilter filter) { 502 this.text = text; 503 504 //Expand regexp for check and message 505 //Does not intern Patterns with Utils.getPattern() 506 String format = ""; 507 try { 508 format = CommonUtil.fillTemplateWithStringsByRegexp( 509 filter.checkFormat, text, filter.commentFormat); 510 tagCheckRegexp = Pattern.compile(format); 511 if (filter.messageFormat == null) { 512 tagMessageRegexp = null; 513 } 514 else { 515 format = CommonUtil.fillTemplateWithStringsByRegexp( 516 filter.messageFormat, text, filter.commentFormat); 517 tagMessageRegexp = Pattern.compile(format); 518 } 519 if (filter.idFormat == null) { 520 tagIdRegexp = null; 521 } 522 else { 523 format = CommonUtil.fillTemplateWithStringsByRegexp( 524 filter.idFormat, text, filter.commentFormat); 525 tagIdRegexp = Pattern.compile(format); 526 } 527 format = CommonUtil.fillTemplateWithStringsByRegexp( 528 filter.influenceFormat, text, filter.commentFormat); 529 530 final int influence = parseInfluence(format, filter.influenceFormat, text); 531 532 if (influence >= 1) { 533 firstLine = line; 534 lastLine = line + influence; 535 } 536 else { 537 firstLine = line + influence; 538 lastLine = line; 539 } 540 } 541 catch (final PatternSyntaxException ex) { 542 throw new IllegalArgumentException( 543 "unable to parse expanded comment " + format, ex); 544 } 545 } 546 547 /** 548 * Gets influence from suppress filter influence format param. 549 * 550 * @param format influence format to parse 551 * @param influenceFormat raw influence format 552 * @param text text of the suppression 553 * @return parsed influence 554 */ 555 private static int parseInfluence(String format, String influenceFormat, String text) { 556 try { 557 return Integer.parseInt(format); 558 } 559 catch (final NumberFormatException ex) { 560 throw new IllegalArgumentException("unable to parse influence from '" + text 561 + "' using " + influenceFormat, ex); 562 } 563 } 564 565 @Override 566 public boolean equals(Object other) { 567 if (this == other) { 568 return true; 569 } 570 if (other == null || getClass() != other.getClass()) { 571 return false; 572 } 573 final Tag tag = (Tag) other; 574 return Objects.equals(firstLine, tag.firstLine) 575 && Objects.equals(lastLine, tag.lastLine) 576 && Objects.equals(text, tag.text) 577 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 578 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp) 579 && Objects.equals(tagIdRegexp, tag.tagIdRegexp); 580 } 581 582 @Override 583 public int hashCode() { 584 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp, 585 tagIdRegexp); 586 } 587 588 /** 589 * Determines whether the source of an audit event 590 * matches the text of this tag. 591 * @param event the {@code TreeWalkerAuditEvent} to check. 592 * @return true if the source of event matches the text of this tag. 593 */ 594 public boolean isMatch(TreeWalkerAuditEvent event) { 595 return isInScopeOfSuppression(event) 596 && isCheckMatch(event) 597 && isIdMatch(event) 598 && isMessageMatch(event); 599 } 600 601 /** 602 * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression. 603 * @param event {@link TreeWalkerAuditEvent} instance. 604 * @return true if the {@link TreeWalkerAuditEvent} is in the scope of the suppression. 605 */ 606 private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) { 607 final int line = event.getLine(); 608 return line >= firstLine && line <= lastLine; 609 } 610 611 /** 612 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format. 613 * @param event {@link TreeWalkerAuditEvent} instance. 614 * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format. 615 */ 616 private boolean isCheckMatch(TreeWalkerAuditEvent event) { 617 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName()); 618 return checkMatcher.find(); 619 } 620 621 /** 622 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format. 623 * @param event {@link TreeWalkerAuditEvent} instance. 624 * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format. 625 */ 626 private boolean isIdMatch(TreeWalkerAuditEvent event) { 627 boolean match = true; 628 if (tagIdRegexp != null) { 629 if (event.getModuleId() == null) { 630 match = false; 631 } 632 else { 633 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId()); 634 match = idMatcher.find(); 635 } 636 } 637 return match; 638 } 639 640 /** 641 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format. 642 * @param event {@link TreeWalkerAuditEvent} instance. 643 * @return true if the {@link TreeWalkerAuditEvent} message matches the message format. 644 */ 645 private boolean isMessageMatch(TreeWalkerAuditEvent event) { 646 boolean match = true; 647 if (tagMessageRegexp != null) { 648 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 649 match = messageMatcher.find(); 650 } 651 return match; 652 } 653 654 @Override 655 public String toString() { 656 return "Tag[text='" + text + '\'' 657 + ", firstLine=" + firstLine 658 + ", lastLine=" + lastLine 659 + ", tagCheckRegexp=" + tagCheckRegexp 660 + ", tagMessageRegexp=" + tagMessageRegexp 661 + ", tagIdRegexp=" + tagIdRegexp 662 + ']'; 663 } 664 665 } 666 667}