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