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