001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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.getLocalizedMessage() != 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 */ 411 private static FileText getFileText(String fileName) { 412 final File file = new File(fileName); 413 FileText result = null; 414 415 // some violations can be on a directory, instead of a file 416 if (!file.isDirectory()) { 417 try { 418 result = new FileText(file, StandardCharsets.UTF_8.name()); 419 } 420 catch (IOException ex) { 421 throw new IllegalStateException("Cannot read source file: " + fileName, ex); 422 } 423 } 424 425 return result; 426 } 427 428 /** 429 * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}. 430 * 431 * @param fileText {@link FileText} instance. 432 * @return list of {@link Suppression} instances. 433 */ 434 private List<Suppression> getSuppressions(FileText fileText) { 435 final List<Suppression> suppressions = new ArrayList<>(); 436 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) { 437 final Optional<Suppression> suppression = getSuppression(fileText, lineNo); 438 suppression.ifPresent(suppressions::add); 439 } 440 return suppressions; 441 } 442 443 /** 444 * Tries to extract the suppression from the given line. 445 * 446 * @param fileText {@link FileText} instance. 447 * @param lineNo line number. 448 * @return {@link Optional} of {@link Suppression}. 449 */ 450 private Optional<Suppression> getSuppression(FileText fileText, int lineNo) { 451 final String line = fileText.get(lineNo); 452 final Matcher onCommentMatcher = onCommentFormat.matcher(line); 453 final Matcher offCommentMatcher = offCommentFormat.matcher(line); 454 455 Suppression suppression = null; 456 if (onCommentMatcher.find()) { 457 suppression = new Suppression(onCommentMatcher.group(0), 458 lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this); 459 } 460 if (offCommentMatcher.find()) { 461 suppression = new Suppression(offCommentMatcher.group(0), 462 lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this); 463 } 464 465 return Optional.ofNullable(suppression); 466 } 467 468 /** 469 * Finds the nearest {@link Suppression} instance which can suppress 470 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope 471 * is before the line and column of the event. 472 * 473 * @param suppressions {@link Suppression} instance. 474 * @param event {@link AuditEvent} instance. 475 * @return {@link Suppression} instance. 476 */ 477 private static Suppression getNearestSuppression(List<Suppression> suppressions, 478 AuditEvent event) { 479 return suppressions 480 .stream() 481 .filter(suppression -> suppression.isMatch(event)) 482 .reduce((first, second) -> second) 483 .filter(suppression -> suppression.suppressionType != SuppressionType.ON) 484 .orElse(null); 485 } 486 487 /** Enum which represents the type of the suppression. */ 488 private enum SuppressionType { 489 490 /** On suppression type. */ 491 ON, 492 /** Off suppression type. */ 493 OFF, 494 495 } 496 497 /** The class which represents the suppression. */ 498 private static final class Suppression { 499 500 /** The regexp which is used to match the event source.*/ 501 private final Pattern eventSourceRegexp; 502 /** The regexp which is used to match the event message.*/ 503 private final Pattern eventMessageRegexp; 504 /** The regexp which is used to match the event ID.*/ 505 private final Pattern eventIdRegexp; 506 507 /** Suppression text.*/ 508 private final String text; 509 /** Suppression line.*/ 510 private final int lineNo; 511 /** Suppression column number.*/ 512 private final int columnNo; 513 /** Suppression type. */ 514 private final SuppressionType suppressionType; 515 516 /** 517 * Creates new suppression instance. 518 * 519 * @param text suppression text. 520 * @param lineNo suppression line number. 521 * @param columnNo suppression column number. 522 * @param suppressionType suppression type. 523 * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context. 524 */ 525 /* package */ Suppression( 526 String text, 527 int lineNo, 528 int columnNo, 529 SuppressionType suppressionType, 530 SuppressWithPlainTextCommentFilter filter 531 ) { 532 this.text = text; 533 this.lineNo = lineNo; 534 this.columnNo = columnNo; 535 this.suppressionType = suppressionType; 536 537 final Pattern commentFormat; 538 if (this.suppressionType == SuppressionType.ON) { 539 commentFormat = filter.onCommentFormat; 540 } 541 else { 542 commentFormat = filter.offCommentFormat; 543 } 544 545 // Expand regexp for check and message 546 // Does not intern Patterns with Utils.getPattern() 547 String format = ""; 548 try { 549 format = CommonUtil.fillTemplateWithStringsByRegexp( 550 filter.checkFormat, text, commentFormat); 551 eventSourceRegexp = Pattern.compile(format); 552 if (filter.messageFormat == null) { 553 eventMessageRegexp = null; 554 } 555 else { 556 format = CommonUtil.fillTemplateWithStringsByRegexp( 557 filter.messageFormat, text, commentFormat); 558 eventMessageRegexp = Pattern.compile(format); 559 } 560 if (filter.idFormat == null) { 561 eventIdRegexp = null; 562 } 563 else { 564 format = CommonUtil.fillTemplateWithStringsByRegexp( 565 filter.idFormat, text, commentFormat); 566 eventIdRegexp = Pattern.compile(format); 567 } 568 } 569 catch (final PatternSyntaxException ex) { 570 throw new IllegalArgumentException( 571 "unable to parse expanded comment " + format, ex); 572 } 573 } 574 575 /** 576 * Indicates whether some other object is "equal to" this one. 577 * Suppression on enumeration is needed so code stays consistent. 578 * 579 * @noinspection EqualsCalledOnEnumConstant 580 */ 581 @Override 582 public boolean equals(Object other) { 583 if (this == other) { 584 return true; 585 } 586 if (other == null || getClass() != other.getClass()) { 587 return false; 588 } 589 final Suppression suppression = (Suppression) other; 590 return Objects.equals(lineNo, suppression.lineNo) 591 && Objects.equals(columnNo, suppression.columnNo) 592 && Objects.equals(suppressionType, suppression.suppressionType) 593 && Objects.equals(text, suppression.text) 594 && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp) 595 && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp) 596 && Objects.equals(eventIdRegexp, suppression.eventIdRegexp); 597 } 598 599 @Override 600 public int hashCode() { 601 return Objects.hash( 602 text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp, 603 eventIdRegexp); 604 } 605 606 /** 607 * Checks whether the suppression matches the given {@link AuditEvent}. 608 * 609 * @param event {@link AuditEvent} instance. 610 * @return true if the suppression matches {@link AuditEvent}. 611 */ 612 private boolean isMatch(AuditEvent event) { 613 return isInScopeOfSuppression(event) 614 && isCheckMatch(event) 615 && isIdMatch(event) 616 && isMessageMatch(event); 617 } 618 619 /** 620 * Checks whether {@link AuditEvent} is in the scope of the suppression. 621 * 622 * @param event {@link AuditEvent} instance. 623 * @return true if {@link AuditEvent} is in the scope of the suppression. 624 */ 625 private boolean isInScopeOfSuppression(AuditEvent event) { 626 return lineNo <= event.getLine(); 627 } 628 629 /** 630 * Checks whether {@link AuditEvent} source name matches the check format. 631 * 632 * @param event {@link AuditEvent} instance. 633 * @return true if the {@link AuditEvent} source name matches the check format. 634 */ 635 private boolean isCheckMatch(AuditEvent event) { 636 final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName()); 637 return checkMatcher.find(); 638 } 639 640 /** 641 * Checks whether the {@link AuditEvent} module ID matches the ID format. 642 * 643 * @param event {@link AuditEvent} instance. 644 * @return true if the {@link AuditEvent} module ID matches the ID format. 645 */ 646 private boolean isIdMatch(AuditEvent event) { 647 boolean match = true; 648 if (eventIdRegexp != null) { 649 if (event.getModuleId() == null) { 650 match = false; 651 } 652 else { 653 final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId()); 654 match = idMatcher.find(); 655 } 656 } 657 return match; 658 } 659 660 /** 661 * Checks whether the {@link AuditEvent} message matches the message format. 662 * 663 * @param event {@link AuditEvent} instance. 664 * @return true if the {@link AuditEvent} message matches the message format. 665 */ 666 private boolean isMessageMatch(AuditEvent event) { 667 boolean match = true; 668 if (eventMessageRegexp != null) { 669 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage()); 670 match = messageMatcher.find(); 671 } 672 return match; 673 } 674 } 675 676}