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