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