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