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