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