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