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