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