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