001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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 if (afterStart && beforeEnd 200 && (nameMatches || checkName.equals(event.getModuleId()))) { 201 suppressed = true; 202 break; 203 } 204 } 205 return suppressed; 206 } 207 208 /** 209 * Checks whether suppression entry position is after the audit event occurrence position 210 * in the source file. 211 * 212 * @param line the line number in the source file where the event occurred. 213 * @param column the column number in the source file where the event occurred. 214 * @param entry suppression entry. 215 * @return true if suppression entry position is after the audit event occurrence position 216 * in the source file. 217 */ 218 private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) { 219 return entry.getFirstLine() < line 220 || entry.getFirstLine() == line 221 && (column == 0 || entry.getFirstColumn() <= column); 222 } 223 224 /** 225 * Checks whether suppression entry position is before the audit event occurrence position 226 * in the source file. 227 * 228 * @param line the line number in the source file where the event occurred. 229 * @param column the column number in the source file where the event occurred. 230 * @param entry suppression entry. 231 * @return true if suppression entry position is before the audit event occurrence position 232 * in the source file. 233 */ 234 private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) { 235 return entry.getLastLine() > line 236 || entry.getLastLine() == line && entry 237 .getLastColumn() >= column; 238 } 239 240 @Override 241 public int[] getDefaultTokens() { 242 return getRequiredTokens(); 243 } 244 245 @Override 246 public int[] getAcceptableTokens() { 247 return getRequiredTokens(); 248 } 249 250 @Override 251 public int[] getRequiredTokens() { 252 return new int[] {TokenTypes.ANNOTATION}; 253 } 254 255 @Override 256 public void beginTree(DetailAST rootAST) { 257 ENTRIES.get().clear(); 258 } 259 260 @Override 261 public void visitToken(DetailAST ast) { 262 // check whether annotation is SuppressWarnings 263 // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN 264 String identifier = getIdentifier(getNthChild(ast, 1)); 265 if (identifier.startsWith(JAVA_LANG_PREFIX)) { 266 identifier = identifier.substring(JAVA_LANG_PREFIX.length()); 267 } 268 if ("SuppressWarnings".equals(identifier)) { 269 getAnnotationTarget(ast).ifPresent(targetAST -> { 270 addSuppressions(getAllAnnotationValues(ast), targetAST); 271 }); 272 } 273 } 274 275 /** 276 * Method to populate list of suppression entries. 277 * 278 * @param values 279 * - list of check names 280 * @param targetAST 281 * - annotation target 282 */ 283 private static void addSuppressions(List<String> values, DetailAST targetAST) { 284 // get text range of target 285 final int firstLine = targetAST.getLineNo(); 286 final int firstColumn = targetAST.getColumnNo(); 287 final DetailAST nextAST = targetAST.getNextSibling(); 288 final int lastLine; 289 final int lastColumn; 290 if (nextAST == null) { 291 lastLine = Integer.MAX_VALUE; 292 lastColumn = Integer.MAX_VALUE; 293 } 294 else { 295 lastLine = nextAST.getLineNo(); 296 lastColumn = nextAST.getColumnNo(); 297 } 298 299 final List<Entry> entries = ENTRIES.get(); 300 for (String value : values) { 301 // strip off the checkstyle-only prefix if present 302 final String checkName = removeCheckstylePrefixIfExists(value); 303 entries.add(new Entry(checkName, firstLine, firstColumn, 304 lastLine, lastColumn)); 305 } 306 } 307 308 /** 309 * Method removes checkstyle prefix (checkstyle:) from check name if exists. 310 * 311 * @param checkName 312 * - name of the check 313 * @return check name without prefix 314 */ 315 private static String removeCheckstylePrefixIfExists(String checkName) { 316 String result = checkName; 317 if (checkName.startsWith(CHECKSTYLE_PREFIX)) { 318 result = checkName.substring(CHECKSTYLE_PREFIX.length()); 319 } 320 return result; 321 } 322 323 /** 324 * Get all annotation values. 325 * 326 * @param ast annotation token 327 * @return list values 328 * @throws IllegalArgumentException if there is an unknown annotation value type. 329 */ 330 private static List<String> getAllAnnotationValues(DetailAST ast) { 331 // get values of annotation 332 List<String> values = Collections.emptyList(); 333 final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN); 334 if (lparenAST != null) { 335 final DetailAST nextAST = lparenAST.getNextSibling(); 336 final int nextType = nextAST.getType(); 337 switch (nextType) { 338 case TokenTypes.EXPR: 339 case TokenTypes.ANNOTATION_ARRAY_INIT: 340 values = getAnnotationValues(nextAST); 341 break; 342 343 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR: 344 // expected children: IDENT ASSIGN ( EXPR | 345 // ANNOTATION_ARRAY_INIT ) 346 values = getAnnotationValues(getNthChild(nextAST, 2)); 347 break; 348 349 case TokenTypes.RPAREN: 350 // no value present (not valid Java) 351 break; 352 353 default: 354 // unknown annotation value type (new syntax?) 355 throw new IllegalArgumentException("Unexpected AST: " + nextAST); 356 } 357 } 358 return values; 359 } 360 361 /** 362 * Get target of annotation. 363 * 364 * @param ast the AST node to get the child of 365 * @return get target of annotation 366 * @throws IllegalArgumentException if there is an unexpected container type. 367 */ 368 private static Optional<DetailAST> getAnnotationTarget(DetailAST ast) { 369 final Optional<DetailAST> result; 370 final DetailAST parentAST = ast.getParent(); 371 switch (parentAST.getType()) { 372 case TokenTypes.MODIFIERS: 373 case TokenTypes.ANNOTATIONS: 374 case TokenTypes.ANNOTATION: 375 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR: 376 result = Optional.of(parentAST.getParent()); 377 break; 378 case TokenTypes.LITERAL_DEFAULT: 379 result = Optional.empty(); 380 break; 381 case TokenTypes.ANNOTATION_ARRAY_INIT: 382 result = getAnnotationTarget(parentAST); 383 break; 384 default: 385 // unexpected container type 386 throw new IllegalArgumentException("Unexpected container AST: " + parentAST); 387 } 388 return result; 389 } 390 391 /** 392 * Returns the n'th child of an AST node. 393 * 394 * @param ast the AST node to get the child of 395 * @param index the index of the child to get 396 * @return the n'th child of the given AST node, or {@code null} if none 397 */ 398 private static DetailAST getNthChild(DetailAST ast, int index) { 399 DetailAST child = ast.getFirstChild(); 400 for (int i = 0; i < index && child != null; ++i) { 401 child = child.getNextSibling(); 402 } 403 return child; 404 } 405 406 /** 407 * Returns the Java identifier represented by an AST. 408 * 409 * @param ast an AST node for an IDENT or DOT 410 * @return the Java identifier represented by the given AST subtree 411 * @throws IllegalArgumentException if the AST is invalid 412 */ 413 private static String getIdentifier(DetailAST ast) { 414 if (ast == null) { 415 throw new IllegalArgumentException("Identifier AST expected, but get null."); 416 } 417 final String identifier; 418 if (ast.getType() == TokenTypes.IDENT) { 419 identifier = ast.getText(); 420 } 421 else { 422 identifier = getIdentifier(ast.getFirstChild()) + "." 423 + getIdentifier(ast.getLastChild()); 424 } 425 return identifier; 426 } 427 428 /** 429 * Returns the literal string expression represented by an AST. 430 * 431 * @param ast an AST node for an EXPR 432 * @return the Java string represented by the given AST expression 433 * or empty string if expression is too complex 434 * @throws IllegalArgumentException if the AST is invalid 435 */ 436 private static String getStringExpr(DetailAST ast) { 437 final DetailAST firstChild = ast.getFirstChild(); 438 String expr = ""; 439 440 switch (firstChild.getType()) { 441 case TokenTypes.STRING_LITERAL: 442 // NOTE: escaped characters are not unescaped 443 final String quotedText = firstChild.getText(); 444 expr = quotedText.substring(1, quotedText.length() - 1); 445 break; 446 case TokenTypes.IDENT: 447 expr = firstChild.getText(); 448 break; 449 case TokenTypes.DOT: 450 expr = firstChild.getLastChild().getText(); 451 break; 452 case TokenTypes.TEXT_BLOCK_LITERAL_BEGIN: 453 final String textBlockContent = firstChild.getFirstChild().getText(); 454 expr = getContentWithoutPrecedingWhitespace(textBlockContent); 455 break; 456 default: 457 // annotations with complex expressions cannot suppress warnings 458 } 459 return expr; 460 } 461 462 /** 463 * Returns the annotation values represented by an AST. 464 * 465 * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT 466 * @return the list of Java string represented by the given AST for an 467 * expression or annotation array initializer 468 * @throws IllegalArgumentException if the AST is invalid 469 */ 470 private static List<String> getAnnotationValues(DetailAST ast) { 471 final List<String> annotationValues; 472 switch (ast.getType()) { 473 case TokenTypes.EXPR: 474 annotationValues = Collections.singletonList(getStringExpr(ast)); 475 break; 476 case TokenTypes.ANNOTATION_ARRAY_INIT: 477 annotationValues = findAllExpressionsInChildren(ast); 478 break; 479 default: 480 throw new IllegalArgumentException( 481 "Expression or annotation array initializer AST expected: " + ast); 482 } 483 return annotationValues; 484 } 485 486 /** 487 * Method looks at children and returns list of expressions in strings. 488 * 489 * @param parent ast, that contains children 490 * @return list of expressions in strings 491 */ 492 private static List<String> findAllExpressionsInChildren(DetailAST parent) { 493 final List<String> valueList = new LinkedList<>(); 494 DetailAST childAST = parent.getFirstChild(); 495 while (childAST != null) { 496 if (childAST.getType() == TokenTypes.EXPR) { 497 valueList.add(getStringExpr(childAST)); 498 } 499 childAST = childAST.getNextSibling(); 500 } 501 return valueList; 502 } 503 504 /** 505 * Remove preceding newline and whitespace from the content of a text block. 506 * 507 * @param textBlockContent the actual text in a text block. 508 * @return content of text block with preceding whitespace and newline removed. 509 */ 510 private static String getContentWithoutPrecedingWhitespace(String textBlockContent) { 511 final String contentWithNoPrecedingNewline = 512 NEWLINE.matcher(textBlockContent).replaceAll(""); 513 return WHITESPACE.matcher(contentWithNoPrecedingNewline).replaceAll(""); 514 } 515 516 @Override 517 public void destroy() { 518 super.destroy(); 519 ENTRIES.remove(); 520 } 521 522 /** Records a particular suppression for a region of a file. */ 523 private static final class Entry { 524 525 /** The source name of the suppressed check. */ 526 private final String checkName; 527 /** The suppression region for the check - first line. */ 528 private final int firstLine; 529 /** The suppression region for the check - first column. */ 530 private final int firstColumn; 531 /** The suppression region for the check - last line. */ 532 private final int lastLine; 533 /** The suppression region for the check - last column. */ 534 private final int lastColumn; 535 536 /** 537 * Constructs a new suppression region entry. 538 * 539 * @param checkName the source name of the suppressed check 540 * @param firstLine the first line of the suppression region 541 * @param firstColumn the first column of the suppression region 542 * @param lastLine the last line of the suppression region 543 * @param lastColumn the last column of the suppression region 544 */ 545 private Entry(String checkName, int firstLine, int firstColumn, 546 int lastLine, int lastColumn) { 547 this.checkName = checkName; 548 this.firstLine = firstLine; 549 this.firstColumn = firstColumn; 550 this.lastLine = lastLine; 551 this.lastColumn = lastColumn; 552 } 553 554 /** 555 * Gets the source name of the suppressed check. 556 * 557 * @return the source name of the suppressed check 558 */ 559 public String getCheckName() { 560 return checkName; 561 } 562 563 /** 564 * Gets the first line of the suppression region. 565 * 566 * @return the first line of the suppression region 567 */ 568 public int getFirstLine() { 569 return firstLine; 570 } 571 572 /** 573 * Gets the first column of the suppression region. 574 * 575 * @return the first column of the suppression region 576 */ 577 public int getFirstColumn() { 578 return firstColumn; 579 } 580 581 /** 582 * Gets the last line of the suppression region. 583 * 584 * @return the last line of the suppression region 585 */ 586 public int getLastLine() { 587 return lastLine; 588 } 589 590 /** 591 * Gets the last column of the suppression region. 592 * 593 * @return the last column of the suppression region 594 */ 595 public int getLastColumn() { 596 return lastColumn; 597 } 598 599 } 600 601}