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