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