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