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