001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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.io.File; 023import java.io.IOException; 024import java.nio.charset.StandardCharsets; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031import java.util.regex.PatternSyntaxException; 032 033import com.puppycrawl.tools.checkstyle.api.AuditEvent; 034import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 035import com.puppycrawl.tools.checkstyle.api.FileText; 036import com.puppycrawl.tools.checkstyle.api.Filter; 037import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 038 039/** 040 * <p> 041 * Filter {@code SuppressWithPlainTextCommentFilter} uses plain text to suppress 042 * audit events. The filter can be used only to suppress audit events received 043 * from the checks which implement FileSetCheck interface. In other words, the 044 * checks which have Checker as a parent module. The filter knows nothing about 045 * AST, it treats only plain text comments and extracts the information required 046 * for suppression from the plain text comments. Currently the filter supports 047 * only single line comments. 048 * </p> 049 * <p> 050 * Please, be aware of the fact that, it is not recommended to use the filter 051 * for Java code anymore, however you still are able to use it to suppress audit 052 * events received from the checks which implement FileSetCheck interface. 053 * </p> 054 * <p> 055 * Rationale: Sometimes there are legitimate reasons for violating a check. 056 * When this is a matter of the code in question and not personal preference, 057 * the best place to override the policy is in the code itself. Semi-structured 058 * comments can be associated with the check. This is sometimes superior to 059 * a separate suppressions file, which must be kept up-to-date as the source 060 * file is edited. 061 * </p> 062 * <p> 063 * Note that the suppression comment should be put before the violation. 064 * You can use more than one suppression comment each on separate line. 065 * </p> 066 * <p> 067 * Properties {@code offCommentFormat} and {@code onCommentFormat} must have equal 068 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Matcher.html#groupCount()"> 069 * paren counts</a>. 070 * </p> 071 * <p> 072 * SuppressionWithPlainTextCommentFilter can suppress Checks that have Treewalker or 073 * Checker as parent module. 074 * </p> 075 * <ul> 076 * <li> 077 * Property {@code offCommentFormat} - Specify comment pattern to trigger filter 078 * to begin suppression. 079 * Type is {@code java.util.regex.Pattern}. 080 * Default value is {@code "// CHECKSTYLE:OFF"}. 081 * </li> 082 * <li> 083 * Property {@code onCommentFormat} - Specify comment pattern to trigger filter 084 * to end suppression. 085 * Type is {@code java.util.regex.Pattern}. 086 * Default value is {@code "// CHECKSTYLE:ON"}. 087 * </li> 088 * <li> 089 * Property {@code checkFormat} - Specify check pattern to suppress. 090 * Type is {@code java.lang.String}. 091 * Default value is {@code ".*"}. 092 * </li> 093 * <li> 094 * Property {@code messageFormat} - Specify message pattern to suppress. 095 * Type is {@code java.lang.String}. 096 * Default value is {@code null}. 097 * </li> 098 * <li> 099 * Property {@code idFormat} - Specify check ID pattern to suppress. 100 * Type is {@code java.lang.String}. 101 * Default value is {@code null}. 102 * </li> 103 * </ul> 104 * <p> 105 * To configure a filter to suppress audit events between a comment containing 106 * {@code CHECKSTYLE:OFF} and a comment containing {@code CHECKSTYLE:ON}: 107 * </p> 108 * <pre> 109 * <module name="Checker"> 110 * ... 111 * <module name="SuppressWithPlainTextCommentFilter"/> 112 * ... 113 * </module> 114 * </pre> 115 * <p> 116 * To configure a filter to suppress audit events between a comment containing 117 * line {@code BEGIN GENERATED CONTENT} and a comment containing line 118 * {@code END GENERATED CONTENT}(Checker is configured to check only properties files): 119 * </p> 120 * <pre> 121 * <module name="Checker"> 122 * <property name="fileExtensions" value="properties"/> 123 * 124 * <module name="SuppressWithPlainTextCommentFilter"> 125 * <property name="offCommentFormat" value="BEGIN GENERATED CONTENT"/> 126 * <property name="onCommentFormat" value="END GENERATED CONTENT"/> 127 * </module> 128 * 129 * </module> 130 * </pre> 131 * <pre> 132 * //BEGIN GENERATED CONTENT 133 * my.property=value1 // No violation events will be reported 134 * my.property=value2 // No violation events will be reported 135 * //END GENERATED CONTENT 136 * . . . 137 * </pre> 138 * <p> 139 * To configure a filter so that {@code -- stop tab check} and {@code -- resume tab check} 140 * marks allowed tab positions (Checker is configured to check only sql files): 141 * </p> 142 * <pre> 143 * <module name="Checker"> 144 * <property name="fileExtensions" value="sql"/> 145 * 146 * <module name="SuppressWithPlainTextCommentFilter"> 147 * <property name="offCommentFormat" value="stop tab check"/> 148 * <property name="onCommentFormat" value="resume tab check"/> 149 * <property name="checkFormat" value="FileTabCharacterCheck"/> 150 * </module> 151 * 152 * </module> 153 * </pre> 154 * <pre> 155 * -- stop tab check 156 * SELECT * FROM users // won't warn here if there is a tab character on line 157 * -- resume tab check 158 * SELECT 1 // will warn here if there is a tab character on line 159 * </pre> 160 * <p> 161 * To configure a filter so that name of suppressed check mentioned in comment 162 * {@code CSOFF: <i>regexp</i>} and {@code CSON: <i>regexp</i>} mark a matching 163 * check (Checker is configured to check only xml files): 164 * </p> 165 * <pre> 166 * <module name="Checker"> 167 * <property name="fileExtensions" value="xml"/> 168 * 169 * <module name="SuppressWithPlainTextCommentFilter"> 170 * <property name="offCommentFormat" value="CSOFF\: ([\w\|]+)"/> 171 * <property name="onCommentFormat" value="CSON\: ([\w\|]+)"/> 172 * <property name="checkFormat" value="$1"/> 173 * </module> 174 * 175 * </module> 176 * </pre> 177 * <pre> 178 * // CSOFF: RegexpSinglelineCheck 179 * // RegexpSingleline check won't warn any lines below here if the line matches regexp 180 * <condition property="checkstyle.ant.skip"> 181 * <isset property="checkstyle.ant.skip"/> 182 * </condition> 183 * // CSON: RegexpSinglelineCheck 184 * // RegexpSingleline check will warn below here if the line matches regexp 185 * <property name="checkstyle.pattern.todo" value="NOTHingWillMatCH_-"/> 186 * </pre> 187 * <p> 188 * To configure a filter to suppress all audit events between a comment containing 189 * {@code CHECKSTYLE_OFF: ALMOST_ALL} and a comment containing {@code CHECKSTYLE_OFF: ALMOST_ALL} 190 * except for the <em>EqualsHashCode</em> check (Checker is configured to check only java files): 191 * </p> 192 * <pre> 193 * <module name="Checker"> 194 * <property name="fileExtensions" value="java"/> 195 * 196 * <module name="SuppressWithPlainTextCommentFilter"> 197 * <property name="offCommentFormat" 198 * value="CHECKSTYLE_OFF: ALMOST_ALL"/> 199 * <property name="onCommentFormat" 200 * value="CHECKSTYLE_ON: ALMOST_ALL"/> 201 * <property name="checkFormat" 202 * value="^((?!(FileTabCharacterCheck)).)*$"/> 203 * </module> 204 * 205 * </module> 206 * </pre> 207 * <pre> 208 * // CHECKSTYLE_OFF: ALMOST_ALL 209 * public static final int array []; 210 * private String [] strArray; 211 * // CHECKSTYLE_ON: ALMOST_ALL 212 * private int array1 []; 213 * </pre> 214 * <p> 215 * To configure a filter to suppress Check's violation message <b>which matches 216 * specified message in messageFormat</b>(so suppression will not be only by 217 * Check's name, but also by message text, as the same Check can report violations 218 * with different message format) between a comment containing {@code stop} and 219 * comment containing {@code resume}: 220 * </p> 221 * <pre> 222 * <module name="Checker"> 223 * <module name="SuppressWithPlainTextCommentFilter"> 224 * <property name="offCommentFormat" value="stop"/> 225 * <property name="onCommentFormat" value="resume"/> 226 * <property name="checkFormat" value="FileTabCharacterCheck"/> 227 * <property name="messageFormat" 228 * value="^File contains tab characters (this is the first instance)\.$"/> 229 * </module> 230 * </module> 231 * </pre> 232 * <p> 233 * It is possible to specify an ID of checks, so that it can be leveraged by the 234 * SuppressWithPlainTextCommentFilter to skip validations. The following examples 235 * show how to skip validations near code that is surrounded with 236 * {@code -- CSOFF <ID> (reason)} and {@code -- CSON <ID>}, 237 * where ID is the ID of checks you want to suppress. 238 * </p> 239 * <p> 240 * Examples of Checkstyle checks configuration: 241 * </p> 242 * <pre> 243 * <module name="RegexpSinglelineJava"> 244 * <property name="id" value="count"/> 245 * <property name="format" value="^.*COUNT(*).*$"/> 246 * <property name="message" 247 * value="Don't use COUNT(*), use COUNT(1) instead."/> 248 * </module> 249 * 250 * <module name="RegexpSinglelineJava"> 251 * <property name="id" value="join"/> 252 * <property name="format" value="^.*JOIN\s.+\s(ON|USING)$"/> 253 * <property name="message" 254 * value="Don't use JOIN, use sub-select instead."/> 255 * </module> 256 * </pre> 257 * <p> 258 * Example of SuppressWithPlainTextCommentFilter configuration (checkFormat which 259 * is set to '$1' points that ID of the checks is in the first group of offCommentFormat 260 * and onCommentFormat regular expressions): 261 * </p> 262 * <pre> 263 * <module name="Checker"> 264 * <property name="fileExtensions" value="sql"/> 265 * 266 * <module name="SuppressWithPlainTextCommentFilter"> 267 * <property name="offCommentFormat" value="CSOFF (\w+) \(\w+\)"/> 268 * <property name="onCommentFormat" value="CSON (\w+)"/> 269 * <property name="idFormat" value="$1"/> 270 * </module> 271 * 272 * </module> 273 * </pre> 274 * <pre> 275 * -- CSOFF join (it is ok to use join here for performance reasons) 276 * SELECT name, job_name 277 * FROM users AS u 278 * JOIN jobs AS j ON u.job_id = j.id 279 * -- CSON join 280 * 281 * -- CSOFF count (test query execution plan) 282 * EXPLAIN SELECT COUNT(*) FROM restaurants 283 * -- CSON count 284 * </pre> 285 * <p> 286 * Example of how to configure the check to suppress more than one check 287 * (Checker is configured to check only sql files). 288 * </p> 289 * <pre> 290 * <module name="Checker"> 291 * <property name="fileExtensions" value="sql"/> 292 * 293 * <module name="SuppressWithPlainTextCommentFilter"> 294 * <property name="offCommentFormat" value="@cs-\: ([\w\|]+)"/> 295 * <property name="checkFormat" value="$1"/> 296 * </module> 297 * 298 * </module> 299 * </pre> 300 * <pre> 301 * -- @cs-: RegexpSinglelineCheck 302 * -- @cs-: FileTabCharacterCheck 303 * CREATE TABLE STATION ( 304 * ID INTEGER PRIMARY KEY, 305 * CITY CHAR(20), 306 * STATE CHAR(2), 307 * LAT_N REAL, 308 * LONG_W REAL); 309 * </pre> 310 * <p> 311 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 312 * </p> 313 * 314 * @since 8.6 315 */ 316public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter { 317 318 /** Comment format which turns checkstyle reporting off. */ 319 private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF"; 320 321 /** Comment format which turns checkstyle reporting on. */ 322 private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON"; 323 324 /** Default check format to suppress. By default the filter suppress all checks. */ 325 private static final String DEFAULT_CHECK_FORMAT = ".*"; 326 327 /** Specify comment pattern to trigger filter to begin suppression. */ 328 private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT); 329 330 /** Specify comment pattern to trigger filter to end suppression. */ 331 private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT); 332 333 /** Specify check pattern to suppress. */ 334 private String checkFormat = DEFAULT_CHECK_FORMAT; 335 336 /** Specify message pattern to suppress. */ 337 private String messageFormat; 338 339 /** Specify check ID pattern to suppress. */ 340 private String idFormat; 341 342 /** 343 * Setter to specify comment pattern to trigger filter to begin suppression. 344 * 345 * @param pattern off comment format pattern. 346 */ 347 public final void setOffCommentFormat(Pattern pattern) { 348 offCommentFormat = pattern; 349 } 350 351 /** 352 * Setter to specify comment pattern to trigger filter to end suppression. 353 * 354 * @param pattern on comment format pattern. 355 */ 356 public final void setOnCommentFormat(Pattern pattern) { 357 onCommentFormat = pattern; 358 } 359 360 /** 361 * Setter to specify check pattern to suppress. 362 * 363 * @param format pattern for check format. 364 */ 365 public final void setCheckFormat(String format) { 366 checkFormat = format; 367 } 368 369 /** 370 * Setter to specify message pattern to suppress. 371 * 372 * @param format pattern for message format. 373 */ 374 public final void setMessageFormat(String format) { 375 messageFormat = format; 376 } 377 378 /** 379 * Setter to specify check ID pattern to suppress. 380 * 381 * @param format pattern for check ID format 382 */ 383 public final void setIdFormat(String format) { 384 idFormat = format; 385 } 386 387 @Override 388 public boolean accept(AuditEvent event) { 389 boolean accepted = true; 390 if (event.getViolation() != null) { 391 final FileText fileText = getFileText(event.getFileName()); 392 if (fileText != null) { 393 final List<Suppression> suppressions = getSuppressions(fileText); 394 accepted = getNearestSuppression(suppressions, event) == null; 395 } 396 } 397 return accepted; 398 } 399 400 @Override 401 protected void finishLocalSetup() { 402 // No code by default 403 } 404 405 /** 406 * Returns {@link FileText} instance created based on the given file name. 407 * 408 * @param fileName the name of the file. 409 * @return {@link FileText} instance. 410 * @throws IllegalStateException if the file could not be read. 411 */ 412 private static FileText getFileText(String fileName) { 413 final File file = new File(fileName); 414 FileText result = null; 415 416 // some violations can be on a directory, instead of a file 417 if (!file.isDirectory()) { 418 try { 419 result = new FileText(file, StandardCharsets.UTF_8.name()); 420 } 421 catch (IOException ex) { 422 throw new IllegalStateException("Cannot read source file: " + fileName, ex); 423 } 424 } 425 426 return result; 427 } 428 429 /** 430 * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}. 431 * 432 * @param fileText {@link FileText} instance. 433 * @return list of {@link Suppression} instances. 434 */ 435 private List<Suppression> getSuppressions(FileText fileText) { 436 final List<Suppression> suppressions = new ArrayList<>(); 437 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) { 438 final Optional<Suppression> suppression = getSuppression(fileText, lineNo); 439 suppression.ifPresent(suppressions::add); 440 } 441 return suppressions; 442 } 443 444 /** 445 * Tries to extract the suppression from the given line. 446 * 447 * @param fileText {@link FileText} instance. 448 * @param lineNo line number. 449 * @return {@link Optional} of {@link Suppression}. 450 */ 451 private Optional<Suppression> getSuppression(FileText fileText, int lineNo) { 452 final String line = fileText.get(lineNo); 453 final Matcher onCommentMatcher = onCommentFormat.matcher(line); 454 final Matcher offCommentMatcher = offCommentFormat.matcher(line); 455 456 Suppression suppression = null; 457 if (onCommentMatcher.find()) { 458 suppression = new Suppression(onCommentMatcher.group(0), 459 lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this); 460 } 461 if (offCommentMatcher.find()) { 462 suppression = new Suppression(offCommentMatcher.group(0), 463 lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this); 464 } 465 466 return Optional.ofNullable(suppression); 467 } 468 469 /** 470 * Finds the nearest {@link Suppression} instance which can suppress 471 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope 472 * is before the line and column of the event. 473 * 474 * @param suppressions {@link Suppression} instance. 475 * @param event {@link AuditEvent} instance. 476 * @return {@link Suppression} instance. 477 */ 478 private static Suppression getNearestSuppression(List<Suppression> suppressions, 479 AuditEvent event) { 480 return suppressions 481 .stream() 482 .filter(suppression -> suppression.isMatch(event)) 483 .reduce((first, second) -> second) 484 .filter(suppression -> suppression.suppressionType != SuppressionType.ON) 485 .orElse(null); 486 } 487 488 /** Enum which represents the type of the suppression. */ 489 private enum SuppressionType { 490 491 /** On suppression type. */ 492 ON, 493 /** Off suppression type. */ 494 OFF, 495 496 } 497 498 /** The class which represents the suppression. */ 499 private static final class Suppression { 500 501 /** The regexp which is used to match the event source.*/ 502 private final Pattern eventSourceRegexp; 503 /** The regexp which is used to match the event message.*/ 504 private final Pattern eventMessageRegexp; 505 /** The regexp which is used to match the event ID.*/ 506 private final Pattern eventIdRegexp; 507 508 /** Suppression text.*/ 509 private final String text; 510 /** Suppression line.*/ 511 private final int lineNo; 512 /** Suppression column number.*/ 513 private final int columnNo; 514 /** Suppression type. */ 515 private final SuppressionType suppressionType; 516 517 /** 518 * Creates new suppression instance. 519 * 520 * @param text suppression text. 521 * @param lineNo suppression line number. 522 * @param columnNo suppression column number. 523 * @param suppressionType suppression type. 524 * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context. 525 * @throws IllegalArgumentException if there is an error in the filter regex syntax. 526 */ 527 /* package */ Suppression( 528 String text, 529 int lineNo, 530 int columnNo, 531 SuppressionType suppressionType, 532 SuppressWithPlainTextCommentFilter filter 533 ) { 534 this.text = text; 535 this.lineNo = lineNo; 536 this.columnNo = columnNo; 537 this.suppressionType = suppressionType; 538 539 final Pattern commentFormat; 540 if (this.suppressionType == SuppressionType.ON) { 541 commentFormat = filter.onCommentFormat; 542 } 543 else { 544 commentFormat = filter.offCommentFormat; 545 } 546 547 // Expand regexp for check and message 548 // Does not intern Patterns with Utils.getPattern() 549 String format = ""; 550 try { 551 format = CommonUtil.fillTemplateWithStringsByRegexp( 552 filter.checkFormat, text, commentFormat); 553 eventSourceRegexp = Pattern.compile(format); 554 if (filter.messageFormat == null) { 555 eventMessageRegexp = null; 556 } 557 else { 558 format = CommonUtil.fillTemplateWithStringsByRegexp( 559 filter.messageFormat, text, commentFormat); 560 eventMessageRegexp = Pattern.compile(format); 561 } 562 if (filter.idFormat == null) { 563 eventIdRegexp = null; 564 } 565 else { 566 format = CommonUtil.fillTemplateWithStringsByRegexp( 567 filter.idFormat, text, commentFormat); 568 eventIdRegexp = Pattern.compile(format); 569 } 570 } 571 catch (final PatternSyntaxException ex) { 572 throw new IllegalArgumentException( 573 "unable to parse expanded comment " + format, ex); 574 } 575 } 576 577 /** 578 * Indicates whether some other object is "equal to" this one. 579 * Suppression on enumeration is needed so code stays consistent. 580 * 581 * @noinspection EqualsCalledOnEnumConstant 582 */ 583 @Override 584 public boolean equals(Object other) { 585 if (this == other) { 586 return true; 587 } 588 if (other == null || getClass() != other.getClass()) { 589 return false; 590 } 591 final Suppression suppression = (Suppression) other; 592 return Objects.equals(lineNo, suppression.lineNo) 593 && Objects.equals(columnNo, suppression.columnNo) 594 && Objects.equals(suppressionType, suppression.suppressionType) 595 && Objects.equals(text, suppression.text) 596 && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp) 597 && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp) 598 && Objects.equals(eventIdRegexp, suppression.eventIdRegexp); 599 } 600 601 @Override 602 public int hashCode() { 603 return Objects.hash( 604 text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp, 605 eventIdRegexp); 606 } 607 608 /** 609 * Checks whether the suppression matches the given {@link AuditEvent}. 610 * 611 * @param event {@link AuditEvent} instance. 612 * @return true if the suppression matches {@link AuditEvent}. 613 */ 614 private boolean isMatch(AuditEvent event) { 615 return isInScopeOfSuppression(event) 616 && isCheckMatch(event) 617 && isIdMatch(event) 618 && isMessageMatch(event); 619 } 620 621 /** 622 * Checks whether {@link AuditEvent} is in the scope of the suppression. 623 * 624 * @param event {@link AuditEvent} instance. 625 * @return true if {@link AuditEvent} is in the scope of the suppression. 626 */ 627 private boolean isInScopeOfSuppression(AuditEvent event) { 628 return lineNo <= event.getLine(); 629 } 630 631 /** 632 * Checks whether {@link AuditEvent} source name matches the check format. 633 * 634 * @param event {@link AuditEvent} instance. 635 * @return true if the {@link AuditEvent} source name matches the check format. 636 */ 637 private boolean isCheckMatch(AuditEvent event) { 638 final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName()); 639 return checkMatcher.find(); 640 } 641 642 /** 643 * Checks whether the {@link AuditEvent} module ID matches the ID format. 644 * 645 * @param event {@link AuditEvent} instance. 646 * @return true if the {@link AuditEvent} module ID matches the ID format. 647 */ 648 private boolean isIdMatch(AuditEvent event) { 649 boolean match = true; 650 if (eventIdRegexp != null) { 651 if (event.getModuleId() == null) { 652 match = false; 653 } 654 else { 655 final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId()); 656 match = idMatcher.find(); 657 } 658 } 659 return match; 660 } 661 662 /** 663 * Checks whether the {@link AuditEvent} message matches the message format. 664 * 665 * @param event {@link AuditEvent} instance. 666 * @return true if the {@link AuditEvent} message matches the message format. 667 */ 668 private boolean isMessageMatch(AuditEvent event) { 669 boolean match = true; 670 if (eventMessageRegexp != null) { 671 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage()); 672 match = messageMatcher.find(); 673 } 674 return match; 675 } 676 } 677 678}