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