View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.BitSet;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.Set;
26  import java.util.regex.Pattern;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.PropertyType;
30  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
31  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32  import com.puppycrawl.tools.checkstyle.api.DetailAST;
33  import com.puppycrawl.tools.checkstyle.api.FullIdent;
34  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
36  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
37  
38  /**
39   * <div>
40   * Checks that particular classes or interfaces are never used.
41   * </div>
42   *
43   * <p>
44   * Rationale: Helps reduce coupling on concrete classes.
45   * </p>
46   *
47   * <p>
48   * For additional restriction of type usage see also:
49   * <a href="https://checkstyle.org/checks/coding/illegalinstantiation.html">
50   * IllegalInstantiation</a>,
51   * <a href="https://checkstyle.org/checks/imports/illegalimport.html">
52   * IllegalImport</a>
53   * </p>
54   *
55   * <p>
56   * Notes:
57   * It is possible to set illegal class names via short or
58   * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-6.html#jls-6.7">canonical</a>
59   * name. Specifying illegal type invokes analyzing imports and Check puts violations at
60   * corresponding declarations (of variables, methods or parameters).
61   * This helps to avoid ambiguous cases, e.g.: {@code java.awt.List} was set as
62   * illegal class name, then, code like:
63   * </p>
64   * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
65   * import java.util.List;
66   * ...
67   * List list; //No violation here
68   * </code></pre></div>
69   *
70   * <p>
71   * will be ok.
72   * </p>
73   *
74   * <p>
75   * In most cases it's justified to put following classes to <b>illegalClassNames</b>:
76   * </p>
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>
86   * as methods that are differ from interface methods are rarely used, so in most cases user will
87   * benefit from checking for them.
88   * </p>
89   *
90   * @since 3.2
91   */
92  @FileStatefulCheck
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     /**
124      * Specify classes that should not be used as types in variable declarations,
125      * return values or parameters.
126      */
127     private final Set<String> illegalClassNames = new HashSet<>();
128     /** Illegal short classes. */
129     private final Set<String> illegalShortClassNames = new HashSet<>();
130     /** Define abstract classes that may be used as types. */
131     private final Set<String> legalAbstractClassNames = new HashSet<>();
132     /** Specify methods that should not be checked. */
133     private final Set<String> ignoredMethodNames = new HashSet<>();
134     /**
135      * Control whether to check only methods and fields with any of the specified modifiers.
136      * This property does not affect method calls nor method references nor record components.
137      */
138     @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
139     private BitSet memberModifiers = new BitSet();
140 
141     /** Specify RegExp for illegal abstract class names. */
142     private Pattern illegalAbstractClassNameFormat = Pattern.compile("^(.*[.])?Abstract.*$");
143 
144     /**
145      * Control whether to validate abstract class names.
146      */
147     private boolean validateAbstractClassNames;
148 
149     /** Creates new instance of the check. */
150     public IllegalTypeCheck() {
151         setIllegalClassNames(DEFAULT_ILLEGAL_TYPES);
152         setIgnoredMethodNames(DEFAULT_IGNORED_METHOD_NAMES);
153     }
154 
155     /**
156      * Setter to specify RegExp for illegal abstract class names.
157      *
158      * @param pattern a pattern.
159      * @since 3.2
160      */
161     public void setIllegalAbstractClassNameFormat(Pattern pattern) {
162         illegalAbstractClassNameFormat = pattern;
163     }
164 
165     /**
166      * Setter to control whether to validate abstract class names.
167      *
168      * @param validateAbstractClassNames whether abstract class names must be ignored.
169      * @since 6.10
170      */
171     public void setValidateAbstractClassNames(boolean validateAbstractClassNames) {
172         this.validateAbstractClassNames = validateAbstractClassNames;
173     }
174 
175     @Override
176     public int[] getDefaultTokens() {
177         return getAcceptableTokens();
178     }
179 
180     @Override
181     public int[] getAcceptableTokens() {
182         return new int[] {
183             TokenTypes.ANNOTATION_FIELD_DEF,
184             TokenTypes.CLASS_DEF,
185             TokenTypes.IMPORT,
186             TokenTypes.INTERFACE_DEF,
187             TokenTypes.METHOD_CALL,
188             TokenTypes.METHOD_DEF,
189             TokenTypes.METHOD_REF,
190             TokenTypes.PARAMETER_DEF,
191             TokenTypes.VARIABLE_DEF,
192             TokenTypes.PATTERN_VARIABLE_DEF,
193             TokenTypes.RECORD_DEF,
194             TokenTypes.RECORD_COMPONENT_DEF,
195             TokenTypes.RECORD_PATTERN_DEF,
196         };
197     }
198 
199     @Override
200     public void beginTree(DetailAST rootAST) {
201         illegalShortClassNames.clear();
202         illegalShortClassNames.addAll(illegalClassNames);
203     }
204 
205     @Override
206     public int[] getRequiredTokens() {
207         return new int[] {TokenTypes.IMPORT};
208     }
209 
210     @Override
211     public void visitToken(DetailAST ast) {
212         switch (ast.getType()) {
213             case TokenTypes.CLASS_DEF,
214                  TokenTypes.INTERFACE_DEF,
215                  TokenTypes.RECORD_DEF -> visitTypeDef(ast);
216 
217             case TokenTypes.METHOD_CALL,
218                  TokenTypes.METHOD_REF -> visitMethodCallOrRef(ast);
219 
220             case TokenTypes.METHOD_DEF -> visitMethodDef(ast);
221 
222             case TokenTypes.VARIABLE_DEF,
223                  TokenTypes.ANNOTATION_FIELD_DEF,
224                  TokenTypes.PATTERN_VARIABLE_DEF -> visitVariableDef(ast);
225 
226             case TokenTypes.RECORD_COMPONENT_DEF,
227                  TokenTypes.RECORD_PATTERN_DEF -> checkClassName(ast);
228 
229             case TokenTypes.PARAMETER_DEF -> visitParameterDef(ast);
230 
231             case TokenTypes.IMPORT -> visitImport(ast);
232 
233             default -> throw new IllegalStateException(ast.toString());
234         }
235     }
236 
237     /**
238      * Checks if current method's return type or variable's type is verifiable
239      * according to <b>memberModifiers</b> option.
240      *
241      * @param methodOrVariableDef METHOD_DEF or VARIABLE_DEF ast node.
242      * @return true if member is verifiable according to <b>memberModifiers</b> option.
243      */
244     private boolean isVerifiable(DetailAST methodOrVariableDef) {
245         boolean result = true;
246         if (!memberModifiers.isEmpty()) {
247             final DetailAST modifiersAst = methodOrVariableDef
248                     .findFirstToken(TokenTypes.MODIFIERS);
249             result = isContainVerifiableType(modifiersAst);
250         }
251         return result;
252     }
253 
254     /**
255      * Checks is modifiers contain verifiable type.
256      *
257      * @param modifiers
258      *            parent node for all modifiers
259      * @return true if method or variable can be verified
260      */
261     private boolean isContainVerifiableType(DetailAST modifiers) {
262         boolean result = false;
263         for (DetailAST modifier = modifiers.getFirstChild(); modifier != null;
264                  modifier = modifier.getNextSibling()) {
265             if (memberModifiers.get(modifier.getType())) {
266                 result = true;
267                 break;
268             }
269         }
270         return result;
271     }
272 
273     /**
274      * Checks the super type and implemented interfaces of a given type.
275      *
276      * @param typeDef class or interface for check.
277      */
278     private void visitTypeDef(DetailAST typeDef) {
279         if (isVerifiable(typeDef)) {
280             checkTypeParameters(typeDef);
281             final DetailAST extendsClause = typeDef.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
282             if (extendsClause != null) {
283                 checkBaseTypes(extendsClause);
284             }
285             final DetailAST implementsClause = typeDef.findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE);
286             if (implementsClause != null) {
287                 checkBaseTypes(implementsClause);
288             }
289         }
290     }
291 
292     /**
293      * Checks return type of a given method.
294      *
295      * @param methodDef method for check.
296      */
297     private void visitMethodDef(DetailAST methodDef) {
298         if (isCheckedMethod(methodDef)) {
299             checkClassName(methodDef);
300         }
301     }
302 
303     /**
304      * Checks type of parameters.
305      *
306      * @param parameterDef parameter list for check.
307      */
308     private void visitParameterDef(DetailAST parameterDef) {
309         final DetailAST grandParentAST = parameterDef.getParent().getParent();
310 
311         if (grandParentAST.getType() == TokenTypes.METHOD_DEF && isCheckedMethod(grandParentAST)) {
312             checkClassName(parameterDef);
313         }
314     }
315 
316     /**
317      * Checks type of given variable.
318      *
319      * @param variableDef variable to check.
320      */
321     private void visitVariableDef(DetailAST variableDef) {
322         if (isVerifiable(variableDef)) {
323             checkClassName(variableDef);
324         }
325     }
326 
327     /**
328      * Checks the type arguments of given method call/reference.
329      *
330      * @param methodCallOrRef method call/reference to check.
331      */
332     private void visitMethodCallOrRef(DetailAST methodCallOrRef) {
333         checkTypeArguments(methodCallOrRef);
334     }
335 
336     /**
337      * Checks imported type (as static and star imports are not supported by Check,
338      *  only type is in the consideration).<br>
339      * If this type is illegal due to Check's options - puts violation on it.
340      *
341      * @param importAst {@link TokenTypes#IMPORT Import}
342      */
343     private void visitImport(DetailAST importAst) {
344         if (!isStarImport(importAst)) {
345             final String canonicalName = getImportedTypeCanonicalName(importAst);
346             extendIllegalClassNamesWithShortName(canonicalName);
347         }
348     }
349 
350     /**
351      * Checks if current import is star import. E.g.:
352      *
353      * <p>
354      * {@code
355      * import java.util.*;
356      * }
357      * </p>
358      *
359      * @param importAst {@link TokenTypes#IMPORT Import}
360      * @return true if it is star import
361      */
362     private static boolean isStarImport(DetailAST importAst) {
363         boolean result = false;
364         DetailAST toVisit = importAst;
365         while (toVisit != null) {
366             toVisit = getNextSubTreeNode(toVisit, importAst);
367             if (toVisit != null && toVisit.getType() == TokenTypes.STAR) {
368                 result = true;
369                 break;
370             }
371         }
372         return result;
373     }
374 
375     /**
376      * Checks type and type arguments/parameters of given method, parameter, variable or
377      * method call/reference.
378      *
379      * @param ast node to check.
380      */
381     private void checkClassName(DetailAST ast) {
382         final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
383         checkType(type);
384         checkTypeParameters(ast);
385     }
386 
387     /**
388      * Checks the identifier of the given type.
389      *
390      * @param type node to check.
391      */
392     private void checkIdent(DetailAST type) {
393         final FullIdent ident = FullIdent.createFullIdent(type);
394         if (isMatchingClassName(ident.getText())) {
395             log(ident.getDetailAst(), MSG_KEY, ident.getText());
396         }
397     }
398 
399     /**
400      * Checks the {@code extends} or {@code implements} statement.
401      *
402      * @param clause DetailAST for either {@link TokenTypes#EXTENDS_CLAUSE} or
403      *               {@link TokenTypes#IMPLEMENTS_CLAUSE}
404      */
405     private void checkBaseTypes(DetailAST clause) {
406         DetailAST child = clause.getFirstChild();
407         while (child != null) {
408             if (child.getType() == TokenTypes.IDENT) {
409                 checkIdent(child);
410             }
411             else {
412                 TokenUtil.forEachChild(child, TokenTypes.TYPE_ARGUMENT, this::checkType);
413             }
414             child = child.getNextSibling();
415         }
416     }
417 
418     /**
419      * Checks the given type, its arguments and parameters.
420      *
421      * @param type node to check.
422      */
423     private void checkType(DetailAST type) {
424         checkIdent(type.getFirstChild());
425         checkTypeArguments(type);
426         checkTypeBounds(type);
427     }
428 
429     /**
430      * Checks the upper and lower bounds for the given type.
431      *
432      * @param type node to check.
433      */
434     private void checkTypeBounds(DetailAST type) {
435         final DetailAST upperBounds = type.findFirstToken(TokenTypes.TYPE_UPPER_BOUNDS);
436         if (upperBounds != null) {
437             checkType(upperBounds);
438         }
439         final DetailAST lowerBounds = type.findFirstToken(TokenTypes.TYPE_LOWER_BOUNDS);
440         if (lowerBounds != null) {
441             checkType(lowerBounds);
442         }
443     }
444 
445     /**
446      * Checks the type parameters of the node.
447      *
448      * @param node node to check.
449      */
450     private void checkTypeParameters(final DetailAST node) {
451         final DetailAST typeParameters = node.findFirstToken(TokenTypes.TYPE_PARAMETERS);
452         if (typeParameters != null) {
453             TokenUtil.forEachChild(typeParameters, TokenTypes.TYPE_PARAMETER, this::checkType);
454         }
455     }
456 
457     /**
458      * Checks the type arguments of the node.
459      *
460      * @param node node to check.
461      */
462     private void checkTypeArguments(final DetailAST node) {
463         DetailAST typeArguments = node.findFirstToken(TokenTypes.TYPE_ARGUMENTS);
464         if (typeArguments == null) {
465             typeArguments = node.getFirstChild().findFirstToken(TokenTypes.TYPE_ARGUMENTS);
466         }
467 
468         if (typeArguments != null) {
469             TokenUtil.forEachChild(typeArguments, TokenTypes.TYPE_ARGUMENT, this::checkType);
470         }
471     }
472 
473     /**
474      * Returns true if given class name is one of illegal classes or else false.
475      *
476      * @param className class name to check.
477      * @return true if given class name is one of illegal classes
478      *         or if it matches to abstract class names pattern.
479      */
480     private boolean isMatchingClassName(String className) {
481         final String shortName = className.substring(className.lastIndexOf('.') + 1);
482         return illegalClassNames.contains(className)
483                 || illegalShortClassNames.contains(shortName)
484                 || validateAbstractClassNames
485                     && !legalAbstractClassNames.contains(className)
486                     && illegalAbstractClassNameFormat.matcher(className).find();
487     }
488 
489     /**
490      * Extends illegal class names set via imported short type name.
491      *
492      * @param canonicalName
493      *     <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7">
494      *     Canonical</a> name of imported type.
495      */
496     private void extendIllegalClassNamesWithShortName(String canonicalName) {
497         if (illegalClassNames.contains(canonicalName)) {
498             final String shortName = canonicalName
499                 .substring(canonicalName.lastIndexOf('.') + 1);
500             illegalShortClassNames.add(shortName);
501         }
502     }
503 
504     /**
505      * Gets imported type's
506      * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.7">
507      *  canonical name</a>.
508      *
509      * @param importAst {@link TokenTypes#IMPORT Import}
510      * @return Imported canonical type's name.
511      */
512     private static String getImportedTypeCanonicalName(DetailAST importAst) {
513         final StringBuilder canonicalNameBuilder = new StringBuilder(256);
514         DetailAST toVisit = importAst;
515         while (toVisit != null) {
516             toVisit = getNextSubTreeNode(toVisit, importAst);
517             if (toVisit != null && toVisit.getType() == TokenTypes.IDENT) {
518                 if (!canonicalNameBuilder.isEmpty()) {
519                     canonicalNameBuilder.append('.');
520                 }
521                 canonicalNameBuilder.append(toVisit.getText());
522             }
523         }
524         return canonicalNameBuilder.toString();
525     }
526 
527     /**
528      * Gets the next node of a syntactical tree (child of a current node or
529      * sibling of a current node, or sibling of a parent of a current node).
530      *
531      * @param currentNodeAst Current node in considering
532      * @param subTreeRootAst SubTree root
533      * @return Current node after bypassing, if current node reached the root of a subtree
534      *        method returns null
535      */
536     private static DetailAST
537         getNextSubTreeNode(DetailAST currentNodeAst, DetailAST subTreeRootAst) {
538         DetailAST currentNode = currentNodeAst;
539         DetailAST toVisitAst = currentNode.getFirstChild();
540         while (toVisitAst == null) {
541             toVisitAst = currentNode.getNextSibling();
542             if (currentNode.getParent().equals(subTreeRootAst)) {
543                 break;
544             }
545             currentNode = currentNode.getParent();
546         }
547         return toVisitAst;
548     }
549 
550     /**
551      * Returns true if method has to be checked or false.
552      *
553      * @param ast method def to check.
554      * @return true if we should check this method.
555      */
556     private boolean isCheckedMethod(DetailAST ast) {
557         final String methodName =
558             ast.findFirstToken(TokenTypes.IDENT).getText();
559         return isVerifiable(ast) && !ignoredMethodNames.contains(methodName)
560                 && !AnnotationUtil.hasOverrideAnnotation(ast);
561     }
562 
563     /**
564      * Setter to specify classes that should not be used as types in variable declarations,
565      * return values or parameters.
566      *
567      * @param classNames array of illegal variable types
568      * @noinspection WeakerAccess
569      * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
570      * @since 3.2
571      */
572     public void setIllegalClassNames(String... classNames) {
573         illegalClassNames.clear();
574         Collections.addAll(illegalClassNames, classNames);
575     }
576 
577     /**
578      * Setter to specify methods that should not be checked.
579      *
580      * @param methodNames array of ignored method names
581      * @noinspection WeakerAccess
582      * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
583      * @since 3.2
584      */
585     public void setIgnoredMethodNames(String... methodNames) {
586         ignoredMethodNames.clear();
587         Collections.addAll(ignoredMethodNames, methodNames);
588     }
589 
590     /**
591      * Setter to define abstract classes that may be used as types.
592      *
593      * @param classNames array of legal abstract class names
594      * @noinspection WeakerAccess
595      * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
596      * @since 4.2
597      */
598     public void setLegalAbstractClassNames(String... classNames) {
599         Collections.addAll(legalAbstractClassNames, classNames);
600     }
601 
602     /**
603      * Setter to control whether to check only methods and fields with any of
604      * the specified modifiers.
605      * This property does not affect method calls nor method references nor record components.
606      *
607      * @param modifiers String contains modifiers.
608      * @since 6.3
609      */
610     public void setMemberModifiers(String modifiers) {
611         memberModifiers = TokenUtil.asBitSet(modifiers.split(","));
612     }
613 
614 }