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