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