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