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