001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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.checks; 021 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Optional; 029import java.util.regex.Pattern; 030 031import com.puppycrawl.tools.checkstyle.StatelessCheck; 032import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 033import com.puppycrawl.tools.checkstyle.api.AuditEvent; 034import com.puppycrawl.tools.checkstyle.api.DetailAST; 035import com.puppycrawl.tools.checkstyle.api.TokenTypes; 036 037/** 038 * <p> 039 * Maintains a set of check suppressions from {@code @SuppressWarnings} annotations. 040 * It allows to prevent Checkstyle from reporting violations from parts of code that were 041 * annotated with {@code @SuppressWarnings} and using name of the check to be excluded. 042 * It is possible to suppress all the checkstyle warnings with the argument {@code "all"}. 043 * You can also use a {@code checkstyle:} prefix to prevent compiler 044 * from processing these annotations. 045 * You can also define aliases for check names that need to be suppressed. 046 * </p> 047 * <ul> 048 * <li> 049 * Property {@code aliasList} - Specify aliases for check names that can be used in code 050 * within {@code SuppressWarnings}. 051 * Type is {@code java.lang.String[]}. 052 * Default value is {@code null}. 053 * </li> 054 * </ul> 055 * <p> 056 * To use default module configuration: 057 * </p> 058 * <pre> 059 * <module name="TreeWalker"> 060 * <module name="MemberName"/> 061 * <module name="ConstantName"/> 062 * <module name="ParameterNumber"> 063 * <property name="id" value="ParamNumberId"/> 064 * </module> 065 * <module name="NoWhitespaceAfter"/> 066 * 067 * <module name="SuppressWarningsHolder"/> 068 * </module> 069 * <module name="SuppressWarningsFilter"/> 070 * </pre> 071 * <pre> 072 * class Test { 073 * 074 * private int K; // violation 075 * @SuppressWarnings({"membername"}) 076 * private int J; // violation suppressed 077 * 078 * private static final int i = 0; // violation 079 * @SuppressWarnings("checkstyle:constantname") 080 * private static final int m = 0; // violation suppressed 081 * 082 * public void needsLotsOfParameters (int a, // violation 083 * int b, int c, int d, int e, int f, int g, int h) { 084 * // ... 085 * } 086 * 087 * @SuppressWarnings("ParamNumberId") 088 * public void needsLotsOfParameters1 (int a, // violation suppressed 089 * int b, int c, int d, int e, int f, int g, int h) { 090 * // ... 091 * } 092 * 093 * private int [] ARR; // 2 violations 094 * @SuppressWarnings("all") 095 * private int [] ARRAY; // violations suppressed 096 * } 097 * </pre> 098 * <p> 099 * The general rule is that the argument of the {@code @SuppressWarnings} will be 100 * matched against class name of the check in any letter case. Adding {@code check} 101 * suffix is also accepted. 102 * </p> 103 * <p> 104 * If {@code aliasList} property was provided you can use your own names e.g. below 105 * code will work if there was provided a {@code ParameterNumberCheck=paramnum} in 106 * the {@code aliasList}: 107 * </p> 108 * <pre> 109 * <module name="TreeWalker"> 110 * <module name="ParameterNumber"/> 111 * 112 * <module name="SuppressWarningsHolder"> 113 * <property name="aliasList" value= 114 * "com.puppycrawl.tools.checkstyle.checks.sizes.ParameterNumberCheck=paramnum"/> 115 * </module> 116 * </module> 117 * <module name="SuppressWarningsFilter"/> 118 * </pre> 119 * <pre> 120 * class Test { 121 * 122 * public void needsLotsOfParameters (int a, // violation 123 * int b, int c, int d, int e, int f, int g, int h) { 124 * // ... 125 * } 126 * 127 * @SuppressWarnings("paramnum") 128 * public void needsLotsOfParameters1 (int a, // violation suppressed 129 * int b, int c, int d, int e, int f, int g, int h) { 130 * // ... 131 * } 132 * 133 * } 134 * </pre> 135 * <p> 136 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 137 * </p> 138 * 139 * @since 5.7 140 */ 141@StatelessCheck 142public class SuppressWarningsHolder 143 extends AbstractCheck { 144 145 /** 146 * Optional prefix for warning suppressions that are only intended to be 147 * recognized by checkstyle. For instance, to suppress {@code 148 * FallThroughCheck} only in checkstyle (and not in javac), use the 149 * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}. 150 * To suppress the warning in both tools, just use {@code "fallthrough"}. 151 */ 152 private static final String CHECKSTYLE_PREFIX = "checkstyle:"; 153 154 /** Java.lang namespace prefix, which is stripped from SuppressWarnings */ 155 private static final String JAVA_LANG_PREFIX = "java.lang."; 156 157 /** Suffix to be removed from subclasses of Check. */ 158 private static final String CHECK_SUFFIX = "check"; 159 160 /** Special warning id for matching all the warnings. */ 161 private static final String ALL_WARNING_MATCHING_ID = "all"; 162 163 /** A map from check source names to suppression aliases. */ 164 private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>(); 165 166 /** 167 * A thread-local holder for the list of suppression entries for the last 168 * file parsed. 169 */ 170 private static final ThreadLocal<List<Entry>> ENTRIES = 171 ThreadLocal.withInitial(LinkedList::new); 172 173 /** 174 * Compiled pattern used to match whitespace in text block content. 175 */ 176 private static final Pattern WHITESPACE = Pattern.compile("\\s+"); 177 178 /** 179 * Compiled pattern used to match preceding newline in text block content. 180 */ 181 private static final Pattern NEWLINE = Pattern.compile("\\n"); 182 183 /** 184 * Returns the default alias for the source name of a check, which is the 185 * source name in lower case with any dotted prefix or "Check"/"check" 186 * suffix removed. 187 * 188 * @param sourceName the source name of the check (generally the class 189 * name) 190 * @return the default alias for the given check 191 */ 192 public static String getDefaultAlias(String sourceName) { 193 int endIndex = sourceName.length(); 194 final String sourceNameLower = sourceName.toLowerCase(Locale.ENGLISH); 195 if (sourceNameLower.endsWith(CHECK_SUFFIX)) { 196 endIndex -= CHECK_SUFFIX.length(); 197 } 198 final int startIndex = sourceNameLower.lastIndexOf('.') + 1; 199 return sourceNameLower.substring(startIndex, endIndex); 200 } 201 202 /** 203 * Returns the alias for the source name of a check. If an alias has been 204 * explicitly registered via {@link #setAliasList(String...)}, that 205 * alias is returned; otherwise, the default alias is used. 206 * 207 * @param sourceName the source name of the check (generally the class 208 * name) 209 * @return the current alias for the given check 210 */ 211 public static String getAlias(String sourceName) { 212 String checkAlias = CHECK_ALIAS_MAP.get(sourceName); 213 if (checkAlias == null) { 214 checkAlias = getDefaultAlias(sourceName); 215 } 216 return checkAlias; 217 } 218 219 /** 220 * Registers an alias for the source name of a check. 221 * 222 * @param sourceName the source name of the check (generally the class 223 * name) 224 * @param checkAlias the alias used in {@link SuppressWarnings} annotations 225 */ 226 private static void registerAlias(String sourceName, String checkAlias) { 227 CHECK_ALIAS_MAP.put(sourceName, checkAlias); 228 } 229 230 /** 231 * Setter to specify aliases for check names that can be used in code 232 * within {@code SuppressWarnings}. 233 * 234 * @param aliasList comma-separated alias assignments 235 * @throws IllegalArgumentException when alias item does not have '=' 236 */ 237 public void setAliasList(String... aliasList) { 238 for (String sourceAlias : aliasList) { 239 final int index = sourceAlias.indexOf('='); 240 if (index > 0) { 241 registerAlias(sourceAlias.substring(0, index), sourceAlias 242 .substring(index + 1)); 243 } 244 else if (!sourceAlias.isEmpty()) { 245 throw new IllegalArgumentException( 246 "'=' expected in alias list item: " + sourceAlias); 247 } 248 } 249 } 250 251 /** 252 * Checks for a suppression of a check with the given source name and 253 * location in the last file processed. 254 * 255 * @param event audit event. 256 * @return whether the check with the given name is suppressed at the given 257 * source location 258 */ 259 public static boolean isSuppressed(AuditEvent event) { 260 final List<Entry> entries = ENTRIES.get(); 261 final String sourceName = event.getSourceName(); 262 final String checkAlias = getAlias(sourceName); 263 final int line = event.getLine(); 264 final int column = event.getColumn(); 265 boolean suppressed = false; 266 for (Entry entry : entries) { 267 final boolean afterStart = isSuppressedAfterEventStart(line, column, entry); 268 final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry); 269 final String checkName = entry.getCheckName(); 270 final boolean nameMatches = 271 ALL_WARNING_MATCHING_ID.equals(checkName) 272 || checkName.equalsIgnoreCase(checkAlias) 273 || getDefaultAlias(checkName).equalsIgnoreCase(checkAlias); 274 if (afterStart && beforeEnd 275 && (nameMatches || checkName.equals(event.getModuleId()))) { 276 suppressed = true; 277 break; 278 } 279 } 280 return suppressed; 281 } 282 283 /** 284 * Checks whether suppression entry position is after the audit event occurrence position 285 * in the source file. 286 * 287 * @param line the line number in the source file where the event occurred. 288 * @param column the column number in the source file where the event occurred. 289 * @param entry suppression entry. 290 * @return true if suppression entry position is after the audit event occurrence position 291 * in the source file. 292 */ 293 private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) { 294 return entry.getFirstLine() < line 295 || entry.getFirstLine() == line 296 && (column == 0 || entry.getFirstColumn() <= column); 297 } 298 299 /** 300 * Checks whether suppression entry position is before the audit event occurrence position 301 * in the source file. 302 * 303 * @param line the line number in the source file where the event occurred. 304 * @param column the column number in the source file where the event occurred. 305 * @param entry suppression entry. 306 * @return true if suppression entry position is before the audit event occurrence position 307 * in the source file. 308 */ 309 private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) { 310 return entry.getLastLine() > line 311 || entry.getLastLine() == line && entry 312 .getLastColumn() >= column; 313 } 314 315 @Override 316 public int[] getDefaultTokens() { 317 return getRequiredTokens(); 318 } 319 320 @Override 321 public int[] getAcceptableTokens() { 322 return getRequiredTokens(); 323 } 324 325 @Override 326 public int[] getRequiredTokens() { 327 return new int[] {TokenTypes.ANNOTATION}; 328 } 329 330 @Override 331 public void beginTree(DetailAST rootAST) { 332 ENTRIES.get().clear(); 333 } 334 335 @Override 336 public void visitToken(DetailAST ast) { 337 // check whether annotation is SuppressWarnings 338 // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN 339 String identifier = getIdentifier(getNthChild(ast, 1)); 340 if (identifier.startsWith(JAVA_LANG_PREFIX)) { 341 identifier = identifier.substring(JAVA_LANG_PREFIX.length()); 342 } 343 if ("SuppressWarnings".equals(identifier)) { 344 getAnnotationTarget(ast).ifPresent(targetAST -> { 345 addSuppressions(getAllAnnotationValues(ast), targetAST); 346 }); 347 } 348 } 349 350 /** 351 * Method to populate list of suppression entries. 352 * 353 * @param values 354 * - list of check names 355 * @param targetAST 356 * - annotation target 357 */ 358 private static void addSuppressions(List<String> values, DetailAST targetAST) { 359 // get text range of target 360 final int firstLine = targetAST.getLineNo(); 361 final int firstColumn = targetAST.getColumnNo(); 362 final DetailAST nextAST = targetAST.getNextSibling(); 363 final int lastLine; 364 final int lastColumn; 365 if (nextAST == null) { 366 lastLine = Integer.MAX_VALUE; 367 lastColumn = Integer.MAX_VALUE; 368 } 369 else { 370 lastLine = nextAST.getLineNo(); 371 lastColumn = nextAST.getColumnNo(); 372 } 373 374 final List<Entry> entries = ENTRIES.get(); 375 for (String value : values) { 376 // strip off the checkstyle-only prefix if present 377 final String checkName = removeCheckstylePrefixIfExists(value); 378 entries.add(new Entry(checkName, firstLine, firstColumn, 379 lastLine, lastColumn)); 380 } 381 } 382 383 /** 384 * Method removes checkstyle prefix (checkstyle:) from check name if exists. 385 * 386 * @param checkName 387 * - name of the check 388 * @return check name without prefix 389 */ 390 private static String removeCheckstylePrefixIfExists(String checkName) { 391 String result = checkName; 392 if (checkName.startsWith(CHECKSTYLE_PREFIX)) { 393 result = checkName.substring(CHECKSTYLE_PREFIX.length()); 394 } 395 return result; 396 } 397 398 /** 399 * Get all annotation values. 400 * 401 * @param ast annotation token 402 * @return list values 403 * @throws IllegalArgumentException if there is an unknown annotation value type. 404 */ 405 private static List<String> getAllAnnotationValues(DetailAST ast) { 406 // get values of annotation 407 List<String> values = Collections.emptyList(); 408 final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN); 409 if (lparenAST != null) { 410 final DetailAST nextAST = lparenAST.getNextSibling(); 411 final int nextType = nextAST.getType(); 412 switch (nextType) { 413 case TokenTypes.EXPR: 414 case TokenTypes.ANNOTATION_ARRAY_INIT: 415 values = getAnnotationValues(nextAST); 416 break; 417 418 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR: 419 // expected children: IDENT ASSIGN ( EXPR | 420 // ANNOTATION_ARRAY_INIT ) 421 values = getAnnotationValues(getNthChild(nextAST, 2)); 422 break; 423 424 case TokenTypes.RPAREN: 425 // no value present (not valid Java) 426 break; 427 428 default: 429 // unknown annotation value type (new syntax?) 430 throw new IllegalArgumentException("Unexpected AST: " + nextAST); 431 } 432 } 433 return values; 434 } 435 436 /** 437 * Get target of annotation. 438 * 439 * @param ast the AST node to get the child of 440 * @return get target of annotation 441 * @throws IllegalArgumentException if there is an unexpected container type. 442 */ 443 private static Optional<DetailAST> getAnnotationTarget(DetailAST ast) { 444 final Optional<DetailAST> result; 445 final DetailAST parentAST = ast.getParent(); 446 switch (parentAST.getType()) { 447 case TokenTypes.MODIFIERS: 448 case TokenTypes.ANNOTATIONS: 449 case TokenTypes.ANNOTATION: 450 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR: 451 result = Optional.of(parentAST.getParent()); 452 break; 453 case TokenTypes.LITERAL_DEFAULT: 454 result = Optional.empty(); 455 break; 456 case TokenTypes.ANNOTATION_ARRAY_INIT: 457 result = getAnnotationTarget(parentAST); 458 break; 459 default: 460 // unexpected container type 461 throw new IllegalArgumentException("Unexpected container AST: " + parentAST); 462 } 463 return result; 464 } 465 466 /** 467 * Returns the n'th child of an AST node. 468 * 469 * @param ast the AST node to get the child of 470 * @param index the index of the child to get 471 * @return the n'th child of the given AST node, or {@code null} if none 472 */ 473 private static DetailAST getNthChild(DetailAST ast, int index) { 474 DetailAST child = ast.getFirstChild(); 475 for (int i = 0; i < index && child != null; ++i) { 476 child = child.getNextSibling(); 477 } 478 return child; 479 } 480 481 /** 482 * Returns the Java identifier represented by an AST. 483 * 484 * @param ast an AST node for an IDENT or DOT 485 * @return the Java identifier represented by the given AST subtree 486 * @throws IllegalArgumentException if the AST is invalid 487 */ 488 private static String getIdentifier(DetailAST ast) { 489 if (ast == null) { 490 throw new IllegalArgumentException("Identifier AST expected, but get null."); 491 } 492 final String identifier; 493 if (ast.getType() == TokenTypes.IDENT) { 494 identifier = ast.getText(); 495 } 496 else { 497 identifier = getIdentifier(ast.getFirstChild()) + "." 498 + getIdentifier(ast.getLastChild()); 499 } 500 return identifier; 501 } 502 503 /** 504 * Returns the literal string expression represented by an AST. 505 * 506 * @param ast an AST node for an EXPR 507 * @return the Java string represented by the given AST expression 508 * or empty string if expression is too complex 509 * @throws IllegalArgumentException if the AST is invalid 510 */ 511 private static String getStringExpr(DetailAST ast) { 512 final DetailAST firstChild = ast.getFirstChild(); 513 String expr = ""; 514 515 switch (firstChild.getType()) { 516 case TokenTypes.STRING_LITERAL: 517 // NOTE: escaped characters are not unescaped 518 final String quotedText = firstChild.getText(); 519 expr = quotedText.substring(1, quotedText.length() - 1); 520 break; 521 case TokenTypes.IDENT: 522 expr = firstChild.getText(); 523 break; 524 case TokenTypes.DOT: 525 expr = firstChild.getLastChild().getText(); 526 break; 527 case TokenTypes.TEXT_BLOCK_LITERAL_BEGIN: 528 final String textBlockContent = firstChild.getFirstChild().getText(); 529 expr = getContentWithoutPrecedingWhitespace(textBlockContent); 530 break; 531 default: 532 // annotations with complex expressions cannot suppress warnings 533 } 534 return expr; 535 } 536 537 /** 538 * Returns the annotation values represented by an AST. 539 * 540 * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT 541 * @return the list of Java string represented by the given AST for an 542 * expression or annotation array initializer 543 * @throws IllegalArgumentException if the AST is invalid 544 */ 545 private static List<String> getAnnotationValues(DetailAST ast) { 546 final List<String> annotationValues; 547 switch (ast.getType()) { 548 case TokenTypes.EXPR: 549 annotationValues = Collections.singletonList(getStringExpr(ast)); 550 break; 551 case TokenTypes.ANNOTATION_ARRAY_INIT: 552 annotationValues = findAllExpressionsInChildren(ast); 553 break; 554 default: 555 throw new IllegalArgumentException( 556 "Expression or annotation array initializer AST expected: " + ast); 557 } 558 return annotationValues; 559 } 560 561 /** 562 * Method looks at children and returns list of expressions in strings. 563 * 564 * @param parent ast, that contains children 565 * @return list of expressions in strings 566 */ 567 private static List<String> findAllExpressionsInChildren(DetailAST parent) { 568 final List<String> valueList = new LinkedList<>(); 569 DetailAST childAST = parent.getFirstChild(); 570 while (childAST != null) { 571 if (childAST.getType() == TokenTypes.EXPR) { 572 valueList.add(getStringExpr(childAST)); 573 } 574 childAST = childAST.getNextSibling(); 575 } 576 return valueList; 577 } 578 579 /** 580 * Remove preceding newline and whitespace from the content of a text block. 581 * 582 * @param textBlockContent the actual text in a text block. 583 * @return content of text block with preceding whitespace and newline removed. 584 */ 585 private static String getContentWithoutPrecedingWhitespace(String textBlockContent) { 586 final String contentWithNoPrecedingNewline = 587 NEWLINE.matcher(textBlockContent).replaceAll(""); 588 return WHITESPACE.matcher(contentWithNoPrecedingNewline).replaceAll(""); 589 } 590 591 @Override 592 public void destroy() { 593 super.destroy(); 594 ENTRIES.remove(); 595 } 596 597 /** Records a particular suppression for a region of a file. */ 598 private static final class Entry { 599 600 /** The source name of the suppressed check. */ 601 private final String checkName; 602 /** The suppression region for the check - first line. */ 603 private final int firstLine; 604 /** The suppression region for the check - first column. */ 605 private final int firstColumn; 606 /** The suppression region for the check - last line. */ 607 private final int lastLine; 608 /** The suppression region for the check - last column. */ 609 private final int lastColumn; 610 611 /** 612 * Constructs a new suppression region entry. 613 * 614 * @param checkName the source name of the suppressed check 615 * @param firstLine the first line of the suppression region 616 * @param firstColumn the first column of the suppression region 617 * @param lastLine the last line of the suppression region 618 * @param lastColumn the last column of the suppression region 619 */ 620 private Entry(String checkName, int firstLine, int firstColumn, 621 int lastLine, int lastColumn) { 622 this.checkName = checkName; 623 this.firstLine = firstLine; 624 this.firstColumn = firstColumn; 625 this.lastLine = lastLine; 626 this.lastColumn = lastColumn; 627 } 628 629 /** 630 * Gets the source name of the suppressed check. 631 * 632 * @return the source name of the suppressed check 633 */ 634 public String getCheckName() { 635 return checkName; 636 } 637 638 /** 639 * Gets the first line of the suppression region. 640 * 641 * @return the first line of the suppression region 642 */ 643 public int getFirstLine() { 644 return firstLine; 645 } 646 647 /** 648 * Gets the first column of the suppression region. 649 * 650 * @return the first column of the suppression region 651 */ 652 public int getFirstColumn() { 653 return firstColumn; 654 } 655 656 /** 657 * Gets the last line of the suppression region. 658 * 659 * @return the last line of the suppression region 660 */ 661 public int getLastLine() { 662 return lastLine; 663 } 664 665 /** 666 * Gets the last column of the suppression region. 667 * 668 * @return the last column of the suppression region 669 */ 670 public int getLastColumn() { 671 return lastColumn; 672 } 673 674 } 675 676}