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