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.coding; 021 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.FullIdent; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 034 035/** 036 * Checks that particular class are never used as types in variable 037 * declarations, return values or parameters. 038 * 039 * <p>Rationale: 040 * Helps reduce coupling on concrete classes. 041 * 042 * <p>Check has following properties: 043 * 044 * <p><b>format</b> - Pattern for illegal class names. 045 * 046 * <p><b>legalAbstractClassNames</b> - Abstract classes that may be used as types. 047 * 048 * <p><b>illegalClassNames</b> - Classes that should not be used as types in variable 049 declarations, return values or parameters. 050 * It is possible to set illegal class names via short or 051 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7"> 052 * canonical</a> name. 053 * Specifying illegal type invokes analyzing imports and Check puts violations at 054 * corresponding declarations 055 * (of variables, methods or parameters). This helps to avoid ambiguous cases, e.g.: 056 * 057 * <p>{@code java.awt.List} was set as illegal class name, then, code like: 058 * 059 * <p>{@code 060 * import java.util.List;<br> 061 * ...<br> 062 * List list; //No violation here 063 * } 064 * 065 * <p>will be ok. 066 * 067 * <p><b>validateAbstractClassNames</b> - controls whether to validate abstract class names. 068 * Default value is <b>false</b> 069 * </p> 070 * 071 * <p><b>ignoredMethodNames</b> - Methods that should not be checked. 072 * 073 * <p><b>memberModifiers</b> - To check only methods and fields with only specified modifiers. 074 * 075 * <p>In most cases it's justified to put following classes to <b>illegalClassNames</b>: 076 * <ul> 077 * <li>GregorianCalendar</li> 078 * <li>Hashtable</li> 079 * <li>ArrayList</li> 080 * <li>LinkedList</li> 081 * <li>Vector</li> 082 * </ul> 083 * 084 * <p>as methods that are differ from interface methods are rear used, so in most cases user will 085 * benefit from checking for them. 086 * </p> 087 * 088 */ 089public final class IllegalTypeCheck extends AbstractCheck { 090 091 /** 092 * A key is pointing to the warning message text in "messages.properties" 093 * file. 094 */ 095 public static final String MSG_KEY = "illegal.type"; 096 097 /** Types illegal by default. */ 098 private static final String[] DEFAULT_ILLEGAL_TYPES = { 099 "HashSet", 100 "HashMap", 101 "LinkedHashMap", 102 "LinkedHashSet", 103 "TreeSet", 104 "TreeMap", 105 "java.util.HashSet", 106 "java.util.HashMap", 107 "java.util.LinkedHashMap", 108 "java.util.LinkedHashSet", 109 "java.util.TreeSet", 110 "java.util.TreeMap", 111 }; 112 113 /** Default ignored method names. */ 114 private static final String[] DEFAULT_IGNORED_METHOD_NAMES = { 115 "getInitialContext", 116 "getEnvironment", 117 }; 118 119 /** Illegal classes. */ 120 private final Set<String> illegalClassNames = new HashSet<>(); 121 /** Illegal short classes. */ 122 private final Set<String> illegalShortClassNames = new HashSet<>(); 123 /** Legal abstract classes. */ 124 private final Set<String> legalAbstractClassNames = new HashSet<>(); 125 /** Methods which should be ignored. */ 126 private final Set<String> ignoredMethodNames = new HashSet<>(); 127 /** Check methods and fields with only corresponding modifiers. */ 128 private List<Integer> memberModifiers; 129 130 /** The regexp to match against. */ 131 private Pattern format = Pattern.compile("^(.*[.])?Abstract.*$"); 132 133 /** 134 * Controls whether to validate abstract class names. 135 */ 136 private boolean validateAbstractClassNames; 137 138 /** Creates new instance of the check. */ 139 public IllegalTypeCheck() { 140 setIllegalClassNames(DEFAULT_ILLEGAL_TYPES); 141 setIgnoredMethodNames(DEFAULT_IGNORED_METHOD_NAMES); 142 } 143 144 /** 145 * Set the format for the specified regular expression. 146 * @param pattern a pattern. 147 */ 148 public void setFormat(Pattern pattern) { 149 format = pattern; 150 } 151 152 /** 153 * Sets whether to validate abstract class names. 154 * @param validateAbstractClassNames whether abstract class names must be ignored. 155 */ 156 public void setValidateAbstractClassNames(boolean validateAbstractClassNames) { 157 this.validateAbstractClassNames = validateAbstractClassNames; 158 } 159 160 @Override 161 public int[] getDefaultTokens() { 162 return getAcceptableTokens(); 163 } 164 165 @Override 166 public int[] getAcceptableTokens() { 167 return new int[] { 168 TokenTypes.VARIABLE_DEF, 169 TokenTypes.PARAMETER_DEF, 170 TokenTypes.METHOD_DEF, 171 TokenTypes.IMPORT, 172 }; 173 } 174 175 @Override 176 public void beginTree(DetailAST rootAST) { 177 illegalShortClassNames.clear(); 178 179 for (String s : illegalClassNames) { 180 if (s.indexOf('.') == -1) { 181 illegalShortClassNames.add(s); 182 } 183 } 184 } 185 186 @Override 187 public int[] getRequiredTokens() { 188 return new int[] {TokenTypes.IMPORT}; 189 } 190 191 @Override 192 public void visitToken(DetailAST ast) { 193 switch (ast.getType()) { 194 case TokenTypes.METHOD_DEF: 195 if (isVerifiable(ast)) { 196 visitMethodDef(ast); 197 } 198 break; 199 case TokenTypes.VARIABLE_DEF: 200 if (isVerifiable(ast)) { 201 visitVariableDef(ast); 202 } 203 break; 204 case TokenTypes.PARAMETER_DEF: 205 visitParameterDef(ast); 206 break; 207 case TokenTypes.IMPORT: 208 visitImport(ast); 209 break; 210 default: 211 throw new IllegalStateException(ast.toString()); 212 } 213 } 214 215 /** 216 * Checks if current method's return type or variable's type is verifiable 217 * according to <b>memberModifiers</b> option. 218 * @param methodOrVariableDef METHOD_DEF or VARIABLE_DEF ast node. 219 * @return true if member is verifiable according to <b>memberModifiers</b> option. 220 */ 221 private boolean isVerifiable(DetailAST methodOrVariableDef) { 222 boolean result = true; 223 if (memberModifiers != null) { 224 final DetailAST modifiersAst = methodOrVariableDef 225 .findFirstToken(TokenTypes.MODIFIERS); 226 result = isContainVerifiableType(modifiersAst); 227 } 228 return result; 229 } 230 231 /** 232 * Checks is modifiers contain verifiable type. 233 * 234 * @param modifiers 235 * parent node for all modifiers 236 * @return true if method or variable can be verified 237 */ 238 private boolean isContainVerifiableType(DetailAST modifiers) { 239 boolean result = false; 240 if (modifiers.getFirstChild() != null) { 241 for (DetailAST modifier = modifiers.getFirstChild(); modifier != null; 242 modifier = modifier.getNextSibling()) { 243 if (memberModifiers.contains(modifier.getType())) { 244 result = true; 245 break; 246 } 247 } 248 } 249 return result; 250 } 251 252 /** 253 * Checks return type of a given method. 254 * @param methodDef method for check. 255 */ 256 private void visitMethodDef(DetailAST methodDef) { 257 if (isCheckedMethod(methodDef)) { 258 checkClassName(methodDef); 259 } 260 } 261 262 /** 263 * Checks type of parameters. 264 * @param parameterDef parameter list for check. 265 */ 266 private void visitParameterDef(DetailAST parameterDef) { 267 final DetailAST grandParentAST = parameterDef.getParent().getParent(); 268 269 if (grandParentAST.getType() == TokenTypes.METHOD_DEF 270 && isCheckedMethod(grandParentAST)) { 271 checkClassName(parameterDef); 272 } 273 } 274 275 /** 276 * Checks type of given variable. 277 * @param variableDef variable to check. 278 */ 279 private void visitVariableDef(DetailAST variableDef) { 280 checkClassName(variableDef); 281 } 282 283 /** 284 * Checks imported type (as static and star imports are not supported by Check, 285 * only type is in the consideration).<br> 286 * If this type is illegal due to Check's options - puts violation on it. 287 * @param importAst {@link TokenTypes#IMPORT Import} 288 */ 289 private void visitImport(DetailAST importAst) { 290 if (!isStarImport(importAst)) { 291 final String canonicalName = getImportedTypeCanonicalName(importAst); 292 extendIllegalClassNamesWithShortName(canonicalName); 293 } 294 } 295 296 /** 297 * Checks if current import is star import. E.g.: 298 * <p> 299 * {@code 300 * import java.util.*; 301 * } 302 * </p> 303 * @param importAst {@link TokenTypes#IMPORT Import} 304 * @return true if it is star import 305 */ 306 private static boolean isStarImport(DetailAST importAst) { 307 boolean result = false; 308 DetailAST toVisit = importAst; 309 while (toVisit != null) { 310 toVisit = getNextSubTreeNode(toVisit, importAst); 311 if (toVisit != null && toVisit.getType() == TokenTypes.STAR) { 312 result = true; 313 break; 314 } 315 } 316 return result; 317 } 318 319 /** 320 * Checks type of given method, parameter or variable. 321 * @param ast node to check. 322 */ 323 private void checkClassName(DetailAST ast) { 324 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 325 final FullIdent ident = FullIdent.createFullIdent(type.getFirstChild()); 326 327 if (isMatchingClassName(ident.getText())) { 328 log(ident.getLineNo(), ident.getColumnNo(), 329 MSG_KEY, ident.getText()); 330 } 331 } 332 333 /** 334 * Returns true if given class name is one of illegal classes or else false. 335 * @param className class name to check. 336 * @return true if given class name is one of illegal classes 337 * or if it matches to abstract class names pattern. 338 */ 339 private boolean isMatchingClassName(String className) { 340 final String shortName = className.substring(className.lastIndexOf('.') + 1); 341 return illegalClassNames.contains(className) 342 || illegalShortClassNames.contains(shortName) 343 || validateAbstractClassNames 344 && !legalAbstractClassNames.contains(className) 345 && format.matcher(className).find(); 346 } 347 348 /** 349 * Extends illegal class names set via imported short type name. 350 * @param canonicalName 351 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7"> 352 * Canonical</a> name of imported type. 353 */ 354 private void extendIllegalClassNamesWithShortName(String canonicalName) { 355 if (illegalClassNames.contains(canonicalName)) { 356 final String shortName = canonicalName 357 .substring(canonicalName.lastIndexOf('.') + 1); 358 illegalShortClassNames.add(shortName); 359 } 360 } 361 362 /** 363 * Gets imported type's 364 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7"> 365 * canonical name</a>. 366 * @param importAst {@link TokenTypes#IMPORT Import} 367 * @return Imported canonical type's name. 368 */ 369 private static String getImportedTypeCanonicalName(DetailAST importAst) { 370 final StringBuilder canonicalNameBuilder = new StringBuilder(256); 371 DetailAST toVisit = importAst; 372 while (toVisit != null) { 373 toVisit = getNextSubTreeNode(toVisit, importAst); 374 if (toVisit != null && toVisit.getType() == TokenTypes.IDENT) { 375 canonicalNameBuilder.append(toVisit.getText()); 376 final DetailAST nextSubTreeNode = getNextSubTreeNode(toVisit, importAst); 377 if (nextSubTreeNode.getType() != TokenTypes.SEMI) { 378 canonicalNameBuilder.append('.'); 379 } 380 } 381 } 382 return canonicalNameBuilder.toString(); 383 } 384 385 /** 386 * Gets the next node of a syntactical tree (child of a current node or 387 * sibling of a current node, or sibling of a parent of a current node). 388 * @param currentNodeAst Current node in considering 389 * @param subTreeRootAst SubTree root 390 * @return Current node after bypassing, if current node reached the root of a subtree 391 * method returns null 392 */ 393 private static DetailAST 394 getNextSubTreeNode(DetailAST currentNodeAst, DetailAST subTreeRootAst) { 395 DetailAST currentNode = currentNodeAst; 396 DetailAST toVisitAst = currentNode.getFirstChild(); 397 while (toVisitAst == null) { 398 toVisitAst = currentNode.getNextSibling(); 399 if (toVisitAst == null) { 400 if (currentNode.getParent().equals(subTreeRootAst)) { 401 break; 402 } 403 currentNode = currentNode.getParent(); 404 } 405 } 406 return toVisitAst; 407 } 408 409 /** 410 * Returns true if method has to be checked or false. 411 * @param ast method def to check. 412 * @return true if we should check this method. 413 */ 414 private boolean isCheckedMethod(DetailAST ast) { 415 final String methodName = 416 ast.findFirstToken(TokenTypes.IDENT).getText(); 417 return !ignoredMethodNames.contains(methodName); 418 } 419 420 /** 421 * Set the list of illegal variable types. 422 * @param classNames array of illegal variable types 423 * @noinspection WeakerAccess 424 */ 425 public void setIllegalClassNames(String... classNames) { 426 illegalClassNames.clear(); 427 Collections.addAll(illegalClassNames, classNames); 428 } 429 430 /** 431 * Set the list of ignore method names. 432 * @param methodNames array of ignored method names 433 * @noinspection WeakerAccess 434 */ 435 public void setIgnoredMethodNames(String... methodNames) { 436 ignoredMethodNames.clear(); 437 Collections.addAll(ignoredMethodNames, methodNames); 438 } 439 440 /** 441 * Set the list of legal abstract class names. 442 * @param classNames array of legal abstract class names 443 * @noinspection WeakerAccess 444 */ 445 public void setLegalAbstractClassNames(String... classNames) { 446 Collections.addAll(legalAbstractClassNames, classNames); 447 } 448 449 /** 450 * Set the list of member modifiers (of methods and fields) which should be checked. 451 * @param modifiers String contains modifiers. 452 */ 453 public void setMemberModifiers(String modifiers) { 454 final List<Integer> modifiersList = new ArrayList<>(); 455 for (String modifier : modifiers.split(",")) { 456 modifiersList.add(TokenUtil.getTokenId(modifier.trim())); 457 } 458 memberModifiers = modifiersList; 459 } 460 461}