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