View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.Arrays;
23  import java.util.HashSet;
24  import java.util.Set;
25  import java.util.stream.Collectors;
26  
27  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
28  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.FullIdent;
31  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
32  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33  
34  /**
35   * <div>
36   * Checks for illegal instantiations where a factory method is preferred.
37   * </div>
38   *
39   * <p>
40   * Rationale: Depending on the project, for some classes it might be
41   * preferable to create instances through factory methods rather than
42   * calling the constructor.
43   * </p>
44   *
45   * <p>
46   * A simple example is the {@code java.lang.Boolean} class.
47   * For performance reasons, it is preferable to use the predefined constants
48   * {@code TRUE} and {@code FALSE}.
49   * Constructor invocations should be replaced by calls to {@code Boolean.valueOf()}.
50   * </p>
51   *
52   * <p>
53   * Some extremely performance sensitive projects may require the use of factory
54   * methods for other classes as well, to enforce the usage of number caches or
55   * object pools.
56   * </p>
57   *
58   * <p>
59   * There is a limitation that it is currently not possible to specify array classes.
60   * </p>
61   * <ul>
62   * <li>
63   * Property {@code classes} - Specify fully qualified class names that should not be instantiated.
64   * Type is {@code java.lang.String[]}.
65   * Default value is {@code ""}.
66   * </li>
67   * </ul>
68   *
69   * <p>
70   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
71   * </p>
72   *
73   * <p>
74   * Violation Message Keys:
75   * </p>
76   * <ul>
77   * <li>
78   * {@code instantiation.avoid}
79   * </li>
80   * </ul>
81   *
82   * @since 3.0
83   */
84  @FileStatefulCheck
85  public class IllegalInstantiationCheck
86      extends AbstractCheck {
87  
88      /**
89       * A key is pointing to the warning message text in "messages.properties"
90       * file.
91       */
92      public static final String MSG_KEY = "instantiation.avoid";
93  
94      /** {@link java.lang} package as string. */
95      private static final String JAVA_LANG = "java.lang.";
96  
97      /** The imports for the file. */
98      private final Set<FullIdent> imports = new HashSet<>();
99  
100     /** The class names defined in the file. */
101     private final Set<String> classNames = new HashSet<>();
102 
103     /** The instantiations in the file. */
104     private final Set<DetailAST> instantiations = new HashSet<>();
105 
106     /** Specify fully qualified class names that should not be instantiated. */
107     private Set<String> classes = new HashSet<>();
108 
109     /** Name of the package. */
110     private String pkgName;
111 
112     @Override
113     public int[] getDefaultTokens() {
114         return getRequiredTokens();
115     }
116 
117     @Override
118     public int[] getAcceptableTokens() {
119         return getRequiredTokens();
120     }
121 
122     @Override
123     public int[] getRequiredTokens() {
124         return new int[] {
125             TokenTypes.IMPORT,
126             TokenTypes.LITERAL_NEW,
127             TokenTypes.PACKAGE_DEF,
128             TokenTypes.CLASS_DEF,
129         };
130     }
131 
132     @Override
133     public void beginTree(DetailAST rootAST) {
134         pkgName = null;
135         imports.clear();
136         instantiations.clear();
137         classNames.clear();
138     }
139 
140     @Override
141     public void visitToken(DetailAST ast) {
142         switch (ast.getType()) {
143             case TokenTypes.LITERAL_NEW:
144                 processLiteralNew(ast);
145                 break;
146             case TokenTypes.PACKAGE_DEF:
147                 processPackageDef(ast);
148                 break;
149             case TokenTypes.IMPORT:
150                 processImport(ast);
151                 break;
152             case TokenTypes.CLASS_DEF:
153                 processClassDef(ast);
154                 break;
155             default:
156                 throw new IllegalArgumentException("Unknown type " + ast);
157         }
158     }
159 
160     @Override
161     public void finishTree(DetailAST rootAST) {
162         instantiations.forEach(this::postProcessLiteralNew);
163     }
164 
165     /**
166      * Collects classes defined in the source file. Required
167      * to avoid false alarms for local vs. java.lang classes.
168      *
169      * @param ast the class def token.
170      */
171     private void processClassDef(DetailAST ast) {
172         final DetailAST identToken = ast.findFirstToken(TokenTypes.IDENT);
173         final String className = identToken.getText();
174         classNames.add(className);
175     }
176 
177     /**
178      * Perform processing for an import token.
179      *
180      * @param ast the import token
181      */
182     private void processImport(DetailAST ast) {
183         final FullIdent name = FullIdent.createFullIdentBelow(ast);
184         // Note: different from UnusedImportsCheck.processImport(),
185         // '.*' imports are also added here
186         imports.add(name);
187     }
188 
189     /**
190      * Perform processing for an package token.
191      *
192      * @param ast the package token
193      */
194     private void processPackageDef(DetailAST ast) {
195         final DetailAST packageNameAST = ast.getLastChild()
196                 .getPreviousSibling();
197         final FullIdent packageIdent =
198                 FullIdent.createFullIdent(packageNameAST);
199         pkgName = packageIdent.getText();
200     }
201 
202     /**
203      * Collects a "new" token.
204      *
205      * @param ast the "new" token
206      */
207     private void processLiteralNew(DetailAST ast) {
208         if (ast.getParent().getType() != TokenTypes.METHOD_REF) {
209             instantiations.add(ast);
210         }
211     }
212 
213     /**
214      * Processes one of the collected "new" tokens when walking tree
215      * has finished.
216      *
217      * @param newTokenAst the "new" token.
218      */
219     private void postProcessLiteralNew(DetailAST newTokenAst) {
220         final DetailAST typeNameAst = newTokenAst.getFirstChild();
221         final DetailAST nameSibling = typeNameAst.getNextSibling();
222         if (nameSibling.getType() != TokenTypes.ARRAY_DECLARATOR) {
223             // ast != "new Boolean[]"
224             final FullIdent typeIdent = FullIdent.createFullIdent(typeNameAst);
225             final String typeName = typeIdent.getText();
226             final String fqClassName = getIllegalInstantiation(typeName);
227             if (fqClassName != null) {
228                 log(newTokenAst, MSG_KEY, fqClassName);
229             }
230         }
231     }
232 
233     /**
234      * Checks illegal instantiations.
235      *
236      * @param className instantiated class, may or may not be qualified
237      * @return the fully qualified class name of className
238      *     or null if instantiation of className is OK
239      */
240     private String getIllegalInstantiation(String className) {
241         String fullClassName = null;
242 
243         if (classes.contains(className)) {
244             fullClassName = className;
245         }
246         else {
247             final int pkgNameLen;
248 
249             if (pkgName == null) {
250                 pkgNameLen = 0;
251             }
252             else {
253                 pkgNameLen = pkgName.length();
254             }
255 
256             for (String illegal : classes) {
257                 if (isSamePackage(className, pkgNameLen, illegal)
258                         || isStandardClass(className, illegal)) {
259                     fullClassName = illegal;
260                 }
261                 else {
262                     fullClassName = checkImportStatements(className);
263                 }
264 
265                 if (fullClassName != null) {
266                     break;
267                 }
268             }
269         }
270         return fullClassName;
271     }
272 
273     /**
274      * Check import statements.
275      *
276      * @param className name of the class
277      * @return value of illegal instantiated type
278      */
279     private String checkImportStatements(String className) {
280         String illegalType = null;
281         // import statements
282         for (FullIdent importLineText : imports) {
283             String importArg = importLineText.getText();
284             if (importArg.endsWith(".*")) {
285                 importArg = importArg.substring(0, importArg.length() - 1)
286                         + className;
287             }
288             if (CommonUtil.baseClassName(importArg).equals(className)
289                     && classes.contains(importArg)) {
290                 illegalType = importArg;
291                 break;
292             }
293         }
294         return illegalType;
295     }
296 
297     /**
298      * Check that type is of the same package.
299      *
300      * @param className class name
301      * @param pkgNameLen package name
302      * @param illegal illegal value
303      * @return true if type of the same package
304      */
305     private boolean isSamePackage(String className, int pkgNameLen, String illegal) {
306         // class from same package
307 
308         // the top level package (pkgName == null) is covered by the
309         // "illegalInstances.contains(className)" check above
310 
311         // the test is the "no garbage" version of
312         // illegal.equals(pkgName + "." + className)
313         return pkgName != null
314                 && className.length() == illegal.length() - pkgNameLen - 1
315                 && illegal.charAt(pkgNameLen) == '.'
316                 && illegal.endsWith(className)
317                 && illegal.startsWith(pkgName);
318     }
319 
320     /**
321      * Is Standard Class.
322      *
323      * @param className class name
324      * @param illegal illegal value
325      * @return true if type is standard
326      */
327     private boolean isStandardClass(String className, String illegal) {
328         boolean isStandardClass = false;
329         // class from java.lang
330         if (illegal.length() - JAVA_LANG.length() == className.length()
331             && illegal.endsWith(className)
332             && illegal.startsWith(JAVA_LANG)) {
333             // java.lang needs no import, but a class without import might
334             // also come from the same file or be in the same package.
335             // E.g. if a class defines an inner class "Boolean",
336             // the expression "new Boolean()" refers to that class,
337             // not to java.lang.Boolean
338 
339             final boolean isSameFile = classNames.contains(className);
340 
341             if (!isSameFile) {
342                 isStandardClass = true;
343             }
344         }
345         return isStandardClass;
346     }
347 
348     /**
349      * Setter to specify fully qualified class names that should not be instantiated.
350      *
351      * @param names class names
352      * @since 3.0
353      */
354     public void setClasses(String... names) {
355         classes = Arrays.stream(names).collect(Collectors.toUnmodifiableSet());
356     }
357 
358 }