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