1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  import java.util.regex.Pattern;
28  
29  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.FullIdent;
33  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
35  
36  /**
37   * Checks that particular classes or interfaces are never used.
38   *
39   * <p>Rationale:
40   * Helps reduce coupling on concrete classes.
41   *
42   * <p>Check has following properties:
43   *
44   * <p><b>illegalAbstractClassNameFormat</b> - Pattern for illegal abstract class names.
45   *
46   * <p><b>legalAbstractClassNames</b> - Abstract classes that may be used as types.
47   *
48   * <p><b>illegalClassNames</b> - Classes that should not be used as types in variable
49     declarations, return values or parameters.
50   * It is possible to set illegal class names via short or
51   * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7">
52   *  canonical</a> name.
53   *  Specifying illegal type invokes analyzing imports and Check puts violations at
54   *   corresponding declarations
55   *  (of variables, methods or parameters). This helps to avoid ambiguous cases, e.g.:
56   *
57   * <p>{@code java.awt.List} was set as illegal class name, then, code like:
58   *
59   * <p>{@code
60   * import java.util.List;<br>
61   * ...<br>
62   * List list; //No violation here
63   * }
64   *
65   * <p>will be ok.
66   *
67   * <p><b>validateAbstractClassNames</b> - controls whether to validate abstract class names.
68   * Default value is <b>false</b>
69   * </p>
70   *
71   * <p><b>ignoredMethodNames</b> - Methods that should not be checked.
72   *
73   * <p><b>memberModifiers</b> - To check only methods and fields with any of the specified modifiers.
74   * This property does not affect method calls nor method references.
75   *
76   * <p>In most cases it's justified to put following classes to <b>illegalClassNames</b>:
77   * <ul>
78   * <li>GregorianCalendar</li>
79   * <li>Hashtable</li>
80   * <li>ArrayList</li>
81   * <li>LinkedList</li>
82   * <li>Vector</li>
83   * </ul>
84   *
85   * <p>as methods that are differ from interface methods are rear used, so in most cases user will
86   *  benefit from checking for them.
87   * </p>
88   *
89   */
90  @FileStatefulCheck
91  public final class IllegalTypeCheck extends AbstractCheck {
92  
93      /**
94       * A key is pointing to the warning message text in "messages.properties"
95       * file.
96       */
97      public static final String MSG_KEY = "illegal.type";
98  
99      /** 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 illegalAbstractClassNameFormat = 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 setIllegalAbstractClassNameFormat(Pattern pattern) {
151         illegalAbstractClassNameFormat = 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.ANNOTATION_FIELD_DEF,
171             TokenTypes.CLASS_DEF,
172             TokenTypes.IMPORT,
173             TokenTypes.INTERFACE_DEF,
174             TokenTypes.METHOD_CALL,
175             TokenTypes.METHOD_DEF,
176             TokenTypes.METHOD_REF,
177             TokenTypes.PARAMETER_DEF,
178             TokenTypes.VARIABLE_DEF,
179         };
180     }
181 
182     @Override
183     public void beginTree(DetailAST rootAST) {
184         illegalShortClassNames.clear();
185 
186         for (String s : illegalClassNames) {
187             if (s.indexOf('.') == -1) {
188                 illegalShortClassNames.add(s);
189             }
190         }
191     }
192 
193     @Override
194     public int[] getRequiredTokens() {
195         return new int[] {TokenTypes.IMPORT};
196     }
197 
198     @Override
199     public void visitToken(DetailAST ast) {
200         switch (ast.getType()) {
201             case TokenTypes.CLASS_DEF:
202             case TokenTypes.INTERFACE_DEF:
203                 visitTypeDef(ast);
204                 break;
205             case TokenTypes.METHOD_CALL:
206             case TokenTypes.METHOD_REF:
207                 visitMethodCallOrRef(ast);
208                 break;
209             case TokenTypes.METHOD_DEF:
210                 visitMethodDef(ast);
211                 break;
212             case TokenTypes.VARIABLE_DEF:
213             case TokenTypes.ANNOTATION_FIELD_DEF:
214                 visitVariableDef(ast);
215                 break;
216             case TokenTypes.PARAMETER_DEF:
217                 visitParameterDef(ast);
218                 break;
219             case TokenTypes.IMPORT:
220                 visitImport(ast);
221                 break;
222             default:
223                 throw new IllegalStateException(ast.toString());
224         }
225     }
226 
227     /**
228      * Checks if current method's return type or variable's type is verifiable
229      * according to <b>memberModifiers</b> option.
230      * @param methodOrVariableDef METHOD_DEF or VARIABLE_DEF ast node.
231      * @return true if member is verifiable according to <b>memberModifiers</b> option.
232      */
233     private boolean isVerifiable(DetailAST methodOrVariableDef) {
234         boolean result = true;
235         if (memberModifiers != null) {
236             final DetailAST modifiersAst = methodOrVariableDef
237                     .findFirstToken(TokenTypes.MODIFIERS);
238             result = isContainVerifiableType(modifiersAst);
239         }
240         return result;
241     }
242 
243     /**
244      * Checks is modifiers contain verifiable type.
245      *
246      * @param modifiers
247      *            parent node for all modifiers
248      * @return true if method or variable can be verified
249      */
250     private boolean isContainVerifiableType(DetailAST modifiers) {
251         boolean result = false;
252         if (modifiers.getFirstChild() != null) {
253             for (DetailAST modifier = modifiers.getFirstChild(); modifier != null;
254                      modifier = modifier.getNextSibling()) {
255                 if (memberModifiers.contains(modifier.getType())) {
256                     result = true;
257                     break;
258                 }
259             }
260         }
261         return result;
262     }
263 
264     /**
265      * Checks the super type and implemented interfaces of a given type.
266      * @param typeDef class or interface for check.
267      */
268     private void visitTypeDef(DetailAST typeDef) {
269         if (isVerifiable(typeDef)) {
270             checkTypeParameters(typeDef);
271             final DetailAST extendsClause = typeDef.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
272             if (extendsClause != null) {
273                 checkBaseTypes(extendsClause);
274             }
275             final DetailAST implementsClause = typeDef.findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE);
276             if (implementsClause != null) {
277                 checkBaseTypes(implementsClause);
278             }
279         }
280     }
281 
282     /**
283      * Checks return type of a given method.
284      * @param methodDef method for check.
285      */
286     private void visitMethodDef(DetailAST methodDef) {
287         if (isVerifiable(methodDef) && isCheckedMethod(methodDef)) {
288             checkClassName(methodDef);
289         }
290     }
291 
292     /**
293      * Checks type of parameters.
294      * @param parameterDef parameter list for check.
295      */
296     private void visitParameterDef(DetailAST parameterDef) {
297         final DetailAST grandParentAST = parameterDef.getParent().getParent();
298 
299         if (grandParentAST.getType() == TokenTypes.METHOD_DEF
300             && isCheckedMethod(grandParentAST)
301             && isVerifiable(grandParentAST)) {
302             checkClassName(parameterDef);
303         }
304     }
305 
306     /**
307      * Checks type of given variable.
308      * @param variableDef variable to check.
309      */
310     private void visitVariableDef(DetailAST variableDef) {
311         if (isVerifiable(variableDef)) {
312             checkClassName(variableDef);
313         }
314     }
315 
316     /**
317      * Checks the type arguments of given method call/reference.
318      * @param methodCallOrRef method call/reference to check.
319      */
320     private void visitMethodCallOrRef(DetailAST methodCallOrRef) {
321         checkTypeArguments(methodCallOrRef);
322     }
323 
324     /**
325      * Checks imported type (as static and star imports are not supported by Check,
326      *  only type is in the consideration).<br>
327      * If this type is illegal due to Check's options - puts violation on it.
328      * @param importAst {@link TokenTypes#IMPORT Import}
329      */
330     private void visitImport(DetailAST importAst) {
331         if (!isStarImport(importAst)) {
332             final String canonicalName = getImportedTypeCanonicalName(importAst);
333             extendIllegalClassNamesWithShortName(canonicalName);
334         }
335     }
336 
337     /**
338      * Checks if current import is star import. E.g.:
339      * <p>
340      * {@code
341      * import java.util.*;
342      * }
343      * </p>
344      * @param importAst {@link TokenTypes#IMPORT Import}
345      * @return true if it is star import
346      */
347     private static boolean isStarImport(DetailAST importAst) {
348         boolean result = false;
349         DetailAST toVisit = importAst;
350         while (toVisit != null) {
351             toVisit = getNextSubTreeNode(toVisit, importAst);
352             if (toVisit != null && toVisit.getType() == TokenTypes.STAR) {
353                 result = true;
354                 break;
355             }
356         }
357         return result;
358     }
359 
360     /**
361      * Checks type and type arguments/parameters of given method, parameter, variable or
362      * method call/reference.
363      * @param ast node to check.
364      */
365     private void checkClassName(DetailAST ast) {
366         final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
367         checkType(type);
368         checkTypeParameters(ast);
369     }
370 
371     /**
372      * Checks the identifier of the given type.
373      * @param type node to check.
374      */
375     private void checkIdent(DetailAST type) {
376         final FullIdent ident = FullIdent.createFullIdent(type);
377         if (isMatchingClassName(ident.getText())) {
378             log(ident.getDetailAst(), MSG_KEY, ident.getText());
379         }
380     }
381 
382     /**
383      * Checks the {@code extends} or {@code implements} statement.
384      * @param clause DetailAST for either {@link TokenTypes#EXTENDS_CLAUSE} or
385      *               {@link TokenTypes#IMPLEMENTS_CLAUSE}
386      */
387     private void checkBaseTypes(DetailAST clause) {
388         DetailAST child = clause.getFirstChild();
389         while (child != null) {
390             if (child.getType() == TokenTypes.IDENT) {
391                 checkIdent(child);
392             }
393             else if (child.getType() == TokenTypes.TYPE_ARGUMENTS) {
394                 TokenUtil.forEachChild(child, TokenTypes.TYPE_ARGUMENT, this::checkType);
395             }
396             child = child.getNextSibling();
397         }
398     }
399 
400     /**
401      * Checks the given type, its arguments and parameters.
402      * @param type node to check.
403      */
404     private void checkType(DetailAST type) {
405         checkIdent(type.getFirstChild());
406         checkTypeArguments(type);
407         checkTypeBounds(type);
408     }
409 
410     /**
411      * Checks the upper and lower bounds for the given type.
412      * @param type node to check.
413      */
414     private void checkTypeBounds(DetailAST type) {
415         final DetailAST upperBounds = type.findFirstToken(TokenTypes.TYPE_UPPER_BOUNDS);
416         if (upperBounds != null) {
417             checkType(upperBounds);
418         }
419         final DetailAST lowerBounds = type.findFirstToken(TokenTypes.TYPE_LOWER_BOUNDS);
420         if (lowerBounds != null) {
421             checkType(lowerBounds);
422         }
423     }
424 
425     /**
426      * Checks the type parameters of the node.
427      * @param node node to check.
428      */
429     private void checkTypeParameters(final DetailAST node) {
430         final DetailAST typeParameters = node.findFirstToken(TokenTypes.TYPE_PARAMETERS);
431         if (typeParameters != null) {
432             TokenUtil.forEachChild(typeParameters, TokenTypes.TYPE_PARAMETER, this::checkType);
433         }
434     }
435 
436     /**
437      * Checks the type arguments of the node.
438      * @param node node to check.
439      */
440     private void checkTypeArguments(final DetailAST node) {
441         DetailAST typeArguments = node.findFirstToken(TokenTypes.TYPE_ARGUMENTS);
442         if (typeArguments == null) {
443             typeArguments = node.getFirstChild().findFirstToken(TokenTypes.TYPE_ARGUMENTS);
444         }
445 
446         if (typeArguments != null) {
447             TokenUtil.forEachChild(typeArguments, TokenTypes.TYPE_ARGUMENT, this::checkType);
448         }
449     }
450 
451     /**
452      * Returns true if given class name is one of illegal classes or else false.
453      * @param className class name to check.
454      * @return true if given class name is one of illegal classes
455      *         or if it matches to abstract class names pattern.
456      */
457     private boolean isMatchingClassName(String className) {
458         final String shortName = className.substring(className.lastIndexOf('.') + 1);
459         return illegalClassNames.contains(className)
460                 || illegalShortClassNames.contains(shortName)
461                 || validateAbstractClassNames
462                     && !legalAbstractClassNames.contains(className)
463                     && illegalAbstractClassNameFormat.matcher(className).find();
464     }
465 
466     /**
467      * Extends illegal class names set via imported short type name.
468      * @param canonicalName
469      *  <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7">
470      *  Canonical</a> name of imported type.
471      */
472     private void extendIllegalClassNamesWithShortName(String canonicalName) {
473         if (illegalClassNames.contains(canonicalName)) {
474             final String shortName = canonicalName
475                 .substring(canonicalName.lastIndexOf('.') + 1);
476             illegalShortClassNames.add(shortName);
477         }
478     }
479 
480     /**
481      * Gets imported type's
482      * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7">
483      *  canonical name</a>.
484      * @param importAst {@link TokenTypes#IMPORT Import}
485      * @return Imported canonical type's name.
486      */
487     private static String getImportedTypeCanonicalName(DetailAST importAst) {
488         final StringBuilder canonicalNameBuilder = new StringBuilder(256);
489         DetailAST toVisit = importAst;
490         while (toVisit != null) {
491             toVisit = getNextSubTreeNode(toVisit, importAst);
492             if (toVisit != null && toVisit.getType() == TokenTypes.IDENT) {
493                 if (canonicalNameBuilder.length() > 0) {
494                     canonicalNameBuilder.append('.');
495                 }
496                 canonicalNameBuilder.append(toVisit.getText());
497             }
498         }
499         return canonicalNameBuilder.toString();
500     }
501 
502     /**
503      * Gets the next node of a syntactical tree (child of a current node or
504      * sibling of a current node, or sibling of a parent of a current node).
505      * @param currentNodeAst Current node in considering
506      * @param subTreeRootAst SubTree root
507      * @return Current node after bypassing, if current node reached the root of a subtree
508      *        method returns null
509      */
510     private static DetailAST
511         getNextSubTreeNode(DetailAST currentNodeAst, DetailAST subTreeRootAst) {
512         DetailAST currentNode = currentNodeAst;
513         DetailAST toVisitAst = currentNode.getFirstChild();
514         while (toVisitAst == null) {
515             toVisitAst = currentNode.getNextSibling();
516             if (toVisitAst == null) {
517                 if (currentNode.getParent().equals(subTreeRootAst)) {
518                     break;
519                 }
520                 currentNode = currentNode.getParent();
521             }
522         }
523         return toVisitAst;
524     }
525 
526     /**
527      * Returns true if method has to be checked or false.
528      * @param ast method def to check.
529      * @return true if we should check this method.
530      */
531     private boolean isCheckedMethod(DetailAST ast) {
532         final String methodName =
533             ast.findFirstToken(TokenTypes.IDENT).getText();
534         return !ignoredMethodNames.contains(methodName);
535     }
536 
537     /**
538      * Set the list of illegal variable types.
539      * @param classNames array of illegal variable types
540      * @noinspection WeakerAccess
541      */
542     public void setIllegalClassNames(String... classNames) {
543         illegalClassNames.clear();
544         Collections.addAll(illegalClassNames, classNames);
545     }
546 
547     /**
548      * Set the list of ignore method names.
549      * @param methodNames array of ignored method names
550      * @noinspection WeakerAccess
551      */
552     public void setIgnoredMethodNames(String... methodNames) {
553         ignoredMethodNames.clear();
554         Collections.addAll(ignoredMethodNames, methodNames);
555     }
556 
557     /**
558      * Set the list of legal abstract class names.
559      * @param classNames array of legal abstract class names
560      * @noinspection WeakerAccess
561      */
562     public void setLegalAbstractClassNames(String... classNames) {
563         Collections.addAll(legalAbstractClassNames, classNames);
564     }
565 
566     /**
567      * Set the list of member modifiers (of methods and fields) which should be checked.
568      * @param modifiers String contains modifiers.
569      */
570     public void setMemberModifiers(String modifiers) {
571         final List<Integer> modifiersList = new ArrayList<>();
572         for (String modifier : modifiers.split(",")) {
573             modifiersList.add(TokenUtil.getTokenId(modifier.trim()));
574         }
575         memberModifiers = modifiersList;
576     }
577 
578 }