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