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