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 * @param className class name to check. 329 * @return true if given class name is one of illegal classes 330 * or if it matches to abstract class names pattern. 331 */ 332 private boolean isMatchingClassName(String className) { 333 final String shortName = className.substring(className.lastIndexOf('.') + 1); 334 return illegalClassNames.contains(className) 335 || illegalClassNames.contains(shortName) 336 || validateAbstractClassNames 337 && !legalAbstractClassNames.contains(className) 338 && format.matcher(className).find(); 339 } 340 341 /** 342 * Extends illegal class names set via imported short type name. 343 * @param canonicalName 344 * <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7"> 345 * Canonical</a> name of imported type. 346 */ 347 private void extendIllegalClassNamesWithShortName(String canonicalName) { 348 if (illegalClassNames.contains(canonicalName)) { 349 final String shortName = canonicalName 350 .substring(canonicalName.lastIndexOf('.') + 1); 351 illegalClassNames.add(shortName); 352 } 353 } 354 355 /** 356 * Gets imported type's 357 * <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7"> 358 * canonical name</a>. 359 * @param importAst {@link TokenTypes#IMPORT Import} 360 * @return Imported canonical type's name. 361 */ 362 private static String getImportedTypeCanonicalName(DetailAST importAst) { 363 final StringBuilder canonicalNameBuilder = new StringBuilder(); 364 DetailAST toVisit = importAst; 365 while (toVisit != null) { 366 toVisit = getNextSubTreeNode(toVisit, importAst); 367 if (toVisit != null && toVisit.getType() == TokenTypes.IDENT) { 368 canonicalNameBuilder.append(toVisit.getText()); 369 final DetailAST nextSubTreeNode = getNextSubTreeNode(toVisit, importAst); 370 if (nextSubTreeNode.getType() != TokenTypes.SEMI) { 371 canonicalNameBuilder.append('.'); 372 } 373 } 374 } 375 return canonicalNameBuilder.toString(); 376 } 377 378 /** 379 * Gets the next node of a syntactical tree (child of a current node or 380 * sibling of a current node, or sibling of a parent of a current node). 381 * @param currentNodeAst Current node in considering 382 * @param subTreeRootAst SubTree root 383 * @return Current node after bypassing, if current node reached the root of a subtree 384 * method returns null 385 */ 386 private static DetailAST 387 getNextSubTreeNode(DetailAST currentNodeAst, DetailAST subTreeRootAst) { 388 DetailAST currentNode = currentNodeAst; 389 DetailAST toVisitAst = currentNode.getFirstChild(); 390 while (toVisitAst == null) { 391 toVisitAst = currentNode.getNextSibling(); 392 if (toVisitAst == null) { 393 if (currentNode.getParent().equals(subTreeRootAst)) { 394 break; 395 } 396 currentNode = currentNode.getParent(); 397 } 398 } 399 return toVisitAst; 400 } 401 402 /** 403 * @param ast method def to check. 404 * @return true if we should check this method. 405 */ 406 private boolean isCheckedMethod(DetailAST ast) { 407 final String methodName = 408 ast.findFirstToken(TokenTypes.IDENT).getText(); 409 return !ignoredMethodNames.contains(methodName); 410 } 411 412 /** 413 * Set the list of illegal variable types. 414 * @param classNames array of illegal variable types 415 */ 416 public void setIllegalClassNames(String... classNames) { 417 illegalClassNames.clear(); 418 Collections.addAll(illegalClassNames, classNames); 419 } 420 421 /** 422 * Set the list of ignore method names. 423 * @param methodNames array of ignored method names 424 */ 425 public void setIgnoredMethodNames(String... methodNames) { 426 ignoredMethodNames.clear(); 427 Collections.addAll(ignoredMethodNames, methodNames); 428 } 429 430 /** 431 * Set the list of legal abstract class names. 432 * @param classNames array of legal abstract class names 433 */ 434 public void setLegalAbstractClassNames(String... classNames) { 435 legalAbstractClassNames.clear(); 436 Collections.addAll(legalAbstractClassNames, classNames); 437 } 438 439 /** 440 * Set the list of member modifiers (of methods and fields) which should be checked. 441 * @param modifiers String contains modifiers. 442 */ 443 public void setMemberModifiers(String modifiers) { 444 final List<Integer> modifiersList = new ArrayList<>(); 445 for (String modifier : modifiers.split(",")) { 446 modifiersList.add(TokenUtils.getTokenId(modifier.trim())); 447 } 448 memberModifiers = modifiersList; 449 } 450}