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