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