/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.typeresolution; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import net.sourceforge.pmd.lang.ast.AbstractNode; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.ast.ASTAdditiveExpression; import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression; import net.sourceforge.pmd.lang.java.ast.ASTAndExpression; import net.sourceforge.pmd.lang.java.ast.ASTAnnotationTypeDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTArrayDimsAndInits; import net.sourceforge.pmd.lang.java.ast.ASTBooleanLiteral; import net.sourceforge.pmd.lang.java.ast.ASTCastExpression; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBody; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTConditionalAndExpression; import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression; import net.sourceforge.pmd.lang.java.ast.ASTConditionalOrExpression; import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTEqualityExpression; import net.sourceforge.pmd.lang.java.ast.ASTExclusiveOrExpression; import net.sourceforge.pmd.lang.java.ast.ASTExpression; import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTInclusiveOrExpression; import net.sourceforge.pmd.lang.java.ast.ASTInstanceOfExpression; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.ast.ASTMarkerAnnotation; import net.sourceforge.pmd.lang.java.ast.ASTMultiplicativeExpression; import net.sourceforge.pmd.lang.java.ast.ASTName; import net.sourceforge.pmd.lang.java.ast.ASTNormalAnnotation; import net.sourceforge.pmd.lang.java.ast.ASTNullLiteral; import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTPostfixExpression; import net.sourceforge.pmd.lang.java.ast.ASTPreDecrementExpression; import net.sourceforge.pmd.lang.java.ast.ASTPreIncrementExpression; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix; import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix; import net.sourceforge.pmd.lang.java.ast.ASTPrimitiveType; import net.sourceforge.pmd.lang.java.ast.ASTReferenceType; import net.sourceforge.pmd.lang.java.ast.ASTRelationalExpression; import net.sourceforge.pmd.lang.java.ast.ASTShiftExpression; import net.sourceforge.pmd.lang.java.ast.ASTSingleMemberAnnotation; import net.sourceforge.pmd.lang.java.ast.ASTStatementExpression; import net.sourceforge.pmd.lang.java.ast.ASTType; import net.sourceforge.pmd.lang.java.ast.ASTTypeDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTUnaryExpression; import net.sourceforge.pmd.lang.java.ast.ASTUnaryExpressionNotPlusMinus; import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator; import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId; import net.sourceforge.pmd.lang.java.ast.JavaParserVisitorAdapter; import net.sourceforge.pmd.lang.java.ast.TypeNode; // // Helpful reading: // http://www.janeg.ca/scjp/oper/promotions.html // http://java.sun.com/docs/books/jls/second_edition/html/conversions.doc.html // public class ClassTypeResolver extends JavaParserVisitorAdapter { private static final Logger LOG = Logger.getLogger(ClassTypeResolver.class.getName()); private static final Map<String, Class<?>> PRIMITIVE_TYPES; private static final Map<String, String> JAVA_LANG; static { // Note: Assumption here that primitives come from same parent // ClassLoader regardless of what ClassLoader we are passed Map<String, Class<?>> thePrimitiveTypes = new HashMap<>(); thePrimitiveTypes.put("void", Void.TYPE); thePrimitiveTypes.put("boolean", Boolean.TYPE); thePrimitiveTypes.put("byte", Byte.TYPE); thePrimitiveTypes.put("char", Character.TYPE); thePrimitiveTypes.put("short", Short.TYPE); thePrimitiveTypes.put("int", Integer.TYPE); thePrimitiveTypes.put("long", Long.TYPE); thePrimitiveTypes.put("float", Float.TYPE); thePrimitiveTypes.put("double", Double.TYPE); PRIMITIVE_TYPES = Collections.unmodifiableMap(thePrimitiveTypes); Map<String, String> theJavaLang = new HashMap<>(); theJavaLang.put("Boolean", "java.lang.Boolean"); theJavaLang.put("Byte", "java.lang.Byte"); theJavaLang.put("Character", "java.lang.Character"); theJavaLang.put("CharSequence", "java.lang.CharSequence"); theJavaLang.put("Class", "java.lang.Class"); theJavaLang.put("ClassLoader", "java.lang.ClassLoader"); theJavaLang.put("Cloneable", "java.lang.Cloneable"); theJavaLang.put("Comparable", "java.lang.Comparable"); theJavaLang.put("Compiler", "java.lang.Compiler"); theJavaLang.put("Double", "java.lang.Double"); theJavaLang.put("Float", "java.lang.Float"); theJavaLang.put("InheritableThreadLocal", "java.lang.InheritableThreadLocal"); theJavaLang.put("Integer", "java.lang.Integer"); theJavaLang.put("Long", "java.lang.Long"); theJavaLang.put("Math", "java.lang.Math"); theJavaLang.put("Number", "java.lang.Number"); theJavaLang.put("Object", "java.lang.Object"); theJavaLang.put("Package", "java.lang.Package"); theJavaLang.put("Process", "java.lang.Process"); theJavaLang.put("Runnable", "java.lang.Runnable"); theJavaLang.put("Runtime", "java.lang.Runtime"); theJavaLang.put("RuntimePermission", "java.lang.RuntimePermission"); theJavaLang.put("SecurityManager", "java.lang.SecurityManager"); theJavaLang.put("Short", "java.lang.Short"); theJavaLang.put("StackTraceElement", "java.lang.StackTraceElement"); theJavaLang.put("StrictMath", "java.lang.StrictMath"); theJavaLang.put("String", "java.lang.String"); theJavaLang.put("StringBuffer", "java.lang.StringBuffer"); theJavaLang.put("System", "java.lang.System"); theJavaLang.put("Thread", "java.lang.Thread"); theJavaLang.put("ThreadGroup", "java.lang.ThreadGroup"); theJavaLang.put("ThreadLocal", "java.lang.ThreadLocal"); theJavaLang.put("Throwable", "java.lang.Throwable"); theJavaLang.put("Void", "java.lang.Void"); JAVA_LANG = Collections.unmodifiableMap(theJavaLang); } private final PMDASMClassLoader pmdClassLoader; private Map<String, String> importedClasses; private List<String> importedOnDemand; private int anonymousClassCounter = 0; public ClassTypeResolver() { this(ClassTypeResolver.class.getClassLoader()); } public ClassTypeResolver(ClassLoader classLoader) { pmdClassLoader = PMDASMClassLoader.getInstance(classLoader); } // FUTURE ASTCompilationUnit should not be a TypeNode. Clean this up // accordingly. @Override public Object visit(ASTCompilationUnit node, Object data) { String className = null; try { importedOnDemand = new ArrayList<>(); importedClasses = new HashMap<>(); className = getClassName(node); if (className != null) { populateClassName(node, className); } } catch (ClassNotFoundException e) { if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Could not find class " + className + ", due to: " + e); } } catch (NoClassDefFoundError e) { if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Could not find class " + className + ", due to: " + e); } } catch (LinkageError e) { if (LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Could not find class " + className + ", due to: " + e); } } finally { populateImports(node); } return super.visit(node, data); } @Override public Object visit(ASTImportDeclaration node, Object data) { ASTName importedType = (ASTName) node.jjtGetChild(0); if (importedType.getType() != null) { node.setType(importedType.getType()); } else { populateType(node, importedType.getImage()); } if (node.getType() != null) { node.setPackage(node.getType().getPackage()); } return data; } @Override public Object visit(ASTTypeDeclaration node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTClassOrInterfaceType node, Object data) { String typeName = node.getImage(); if (node.jjtGetParent().hasDescendantOfType(ASTClassOrInterfaceBody.class)) { anonymousClassCounter++; AbstractNode parent = node.getFirstParentOfType(ASTClassOrInterfaceDeclaration.class); if (parent == null) { parent = node.getFirstParentOfType(ASTEnumDeclaration.class); } typeName = parent.getImage() + "$" + anonymousClassCounter; } populateType(node, typeName); return data; } @Override public Object visit(ASTClassOrInterfaceDeclaration node, Object data) { populateType(node, node.getImage()); return super.visit(node, data); } @Override public Object visit(ASTEnumDeclaration node, Object data) { populateType(node, node.getImage()); return super.visit(node, data); } @Override public Object visit(ASTAnnotationTypeDeclaration node, Object data) { populateType(node, node.getImage()); return super.visit(node, data); } @Override public Object visit(ASTName node, Object data) { /* * Only doing this for nodes where getNameDeclaration is null this means * it's not a named node, i.e. Static reference or Annotation Doing this * for memory - TODO: Investigate if there is a valid memory concern or * not */ if (node.getNameDeclaration() == null) { // Skip these scenarios as there is no type to populate in these // cases: // 1) Parent is a PackageDeclaration, which is not a type // 2) Parent is a ImportDeclaration, this is handled elsewhere. if (!(node.jjtGetParent() instanceof ASTPackageDeclaration || node.jjtGetParent() instanceof ASTImportDeclaration)) { String name = node.getImage(); if (name.indexOf('.') != -1) { name = name.substring(0, name.indexOf('.')); } populateType(node, name); } } else { // Carry over the type from the declaration if (node.getNameDeclaration().getNode() instanceof TypeNode) { node.setType(((TypeNode) node.getNameDeclaration().getNode()).getType()); } } return super.visit(node, data); } @Override public Object visit(ASTFieldDeclaration node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTVariableDeclarator node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTVariableDeclaratorId node, Object data) { if (node == null || node.getNameDeclaration() == null) { return super.visit(node, data); } String name = node.getNameDeclaration().getTypeImage(); if (name != null) { populateType(node, name); } return super.visit(node, data); } @Override public Object visit(ASTType node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTReferenceType node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTPrimitiveType node, Object data) { populateType(node, node.getImage()); return super.visit(node, data); } @Override public Object visit(ASTExpression node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTConditionalExpression node, Object data) { super.visit(node, data); if (node.isTernary()) { // TODO Rules for Ternary are complex } else { rollupTypeUnary(node); } return data; } @Override public Object visit(ASTConditionalOrExpression node, Object data) { populateType(node, "boolean"); return super.visit(node, data); } @Override public Object visit(ASTConditionalAndExpression node, Object data) { populateType(node, "boolean"); return super.visit(node, data); } @Override public Object visit(ASTInclusiveOrExpression node, Object data) { super.visit(node, data); rollupTypeBinaryNumericPromotion(node); return data; } @Override public Object visit(ASTExclusiveOrExpression node, Object data) { super.visit(node, data); rollupTypeBinaryNumericPromotion(node); return data; } @Override public Object visit(ASTAndExpression node, Object data) { super.visit(node, data); rollupTypeBinaryNumericPromotion(node); return data; } @Override public Object visit(ASTEqualityExpression node, Object data) { populateType(node, "boolean"); return super.visit(node, data); } @Override public Object visit(ASTInstanceOfExpression node, Object data) { populateType(node, "boolean"); return super.visit(node, data); } @Override public Object visit(ASTRelationalExpression node, Object data) { populateType(node, "boolean"); return super.visit(node, data); } @Override public Object visit(ASTShiftExpression node, Object data) { super.visit(node, data); // Unary promotion on LHS is type of a shift operation rollupTypeUnaryNumericPromotion(node); return data; } @Override public Object visit(ASTAdditiveExpression node, Object data) { super.visit(node, data); rollupTypeBinaryNumericPromotion(node); return data; } @Override public Object visit(ASTMultiplicativeExpression node, Object data) { super.visit(node, data); rollupTypeBinaryNumericPromotion(node); return data; } @Override public Object visit(ASTUnaryExpression node, Object data) { super.visit(node, data); rollupTypeUnaryNumericPromotion(node); return data; } @Override public Object visit(ASTPreIncrementExpression node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTPreDecrementExpression node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTUnaryExpressionNotPlusMinus node, Object data) { super.visit(node, data); if ("!".equals(node.getImage())) { populateType(node, "boolean"); } else { rollupTypeUnary(node); } return data; } @Override public Object visit(ASTPostfixExpression node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTCastExpression node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTPrimaryExpression node, Object data) { super.visit(node, data); if (node.jjtGetNumChildren() == 1) { rollupTypeUnary(node); } else { // TODO OMG, this is complicated. PrimaryExpression, PrimaryPrefix // and PrimarySuffix are all related. } return data; } @Override public Object visit(ASTPrimaryPrefix node, Object data) { super.visit(node, data); if (node.getImage() == null) { rollupTypeUnary(node); } else { // TODO OMG, this is complicated. PrimaryExpression, PrimaryPrefix // and PrimarySuffix are all related. } return data; } @Override public Object visit(ASTPrimarySuffix node, Object data) { super.visit(node, data); // TODO OMG, this is complicated. PrimaryExpression, PrimaryPrefix and // PrimarySuffix are all related. return data; } @Override public Object visit(ASTNullLiteral node, Object data) { // No explicit type return super.visit(node, data); } @Override public Object visit(ASTBooleanLiteral node, Object data) { populateType(node, "boolean"); return super.visit(node, data); } @Override public Object visit(ASTLiteral node, Object data) { super.visit(node, data); if (node.jjtGetNumChildren() != 0) { rollupTypeUnary(node); } else { if (node.isIntLiteral()) { populateType(node, "int"); } else if (node.isLongLiteral()) { populateType(node, "long"); } else if (node.isFloatLiteral()) { populateType(node, "float"); } else if (node.isDoubleLiteral()) { populateType(node, "double"); } else if (node.isCharLiteral()) { populateType(node, "char"); } else if (node.isStringLiteral()) { populateType(node, "java.lang.String"); } else { throw new IllegalStateException("PMD error, unknown literal type!"); } } return data; } @Override public Object visit(ASTAllocationExpression node, Object data) { super.visit(node, data); if (node.jjtGetNumChildren() >= 2 && node.jjtGetChild(1) instanceof ASTArrayDimsAndInits || node.jjtGetNumChildren() >= 3 && node.jjtGetChild(2) instanceof ASTArrayDimsAndInits) { // // Classes for Array types cannot be found directly using // reflection. // As far as I can tell you have to create an array instance of the // necessary // dimensionality, and then ask for the type from the instance. OMFG // that's ugly. // // TODO Need to create utility method to allow array type creation // which will use // caching to avoid repeated object creation. // TODO Modify Parser to tell us array dimensions count. // TODO Parser seems to do some work to handle arrays in certain // case already. // Examine those to figure out what's going on, make sure _all_ // array scenarios // are ultimately covered. Appears to use a Dimensionable interface // to handle // only a part of the APIs (not bump), but is implemented several // times, so // look at refactoring to eliminate duplication. Dimensionable is // also used // on AccessNodes for some scenarios, need to account for that. // Might be // missing some TypeNode candidates we can add to the AST and have // to deal // with here (e.g. FormalParameter)? Plus some existing usages may // be // incorrect. } else { rollupTypeUnary(node); } return data; } @Override public Object visit(ASTStatementExpression node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTNormalAnnotation node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTMarkerAnnotation node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } @Override public Object visit(ASTSingleMemberAnnotation node, Object data) { super.visit(node, data); rollupTypeUnary(node); return data; } // Roll up the type based on type of the first child node. private void rollupTypeUnary(TypeNode typeNode) { Node node = typeNode; if (node.jjtGetNumChildren() >= 1) { Node child = node.jjtGetChild(0); if (child instanceof TypeNode) { typeNode.setType(((TypeNode) child).getType()); } } } // Roll up the type based on type of the first child node using Unary // Numeric Promotion per JLS 5.6.1 private void rollupTypeUnaryNumericPromotion(TypeNode typeNode) { Node node = typeNode; if (node.jjtGetNumChildren() >= 1) { Node child = node.jjtGetChild(0); if (child instanceof TypeNode) { Class<?> type = ((TypeNode) child).getType(); if (type != null) { if ("byte".equals(type.getName()) || "short".equals(type.getName()) || "char".equals(type.getName())) { populateType(typeNode, "int"); } else { typeNode.setType(((TypeNode) child).getType()); } } } } } // Roll up the type based on type of the first and second child nodes using // Binary Numeric Promotion per JLS 5.6.2 private void rollupTypeBinaryNumericPromotion(TypeNode typeNode) { Node node = typeNode; if (node.jjtGetNumChildren() >= 2) { Node child1 = node.jjtGetChild(0); Node child2 = node.jjtGetChild(1); if (child1 instanceof TypeNode && child2 instanceof TypeNode) { Class<?> type1 = ((TypeNode) child1).getType(); Class<?> type2 = ((TypeNode) child2).getType(); if (type1 != null && type2 != null) { // Yeah, String is not numeric, but easiest place to handle // it, only affects ASTAdditiveExpression if ("java.lang.String".equals(type1.getName()) || "java.lang.String".equals(type2.getName())) { populateType(typeNode, "java.lang.String"); } else if ("boolean".equals(type1.getName()) || "boolean".equals(type2.getName())) { populateType(typeNode, "boolean"); } else if ("double".equals(type1.getName()) || "double".equals(type2.getName())) { populateType(typeNode, "double"); } else if ("float".equals(type1.getName()) || "float".equals(type2.getName())) { populateType(typeNode, "float"); } else if ("long".equals(type1.getName()) || "long".equals(type2.getName())) { populateType(typeNode, "long"); } else { populateType(typeNode, "int"); } } else if (type1 != null || type2 != null) { // If one side is known to be a String, then the result is a // String // Yeah, String is not numeric, but easiest place to handle // it, only affects ASTAdditiveExpression if (type1 != null && "java.lang.String".equals(type1.getName()) || type2 != null && "java.lang.String".equals(type2.getName())) { populateType(typeNode, "java.lang.String"); } } } } } private void populateType(TypeNode node, String className) { String qualifiedName = className; Class<?> myType = PRIMITIVE_TYPES.get(className); if (myType == null && importedClasses != null) { if (importedClasses.containsKey(className)) { qualifiedName = importedClasses.get(className); } else if (importedClasses.containsValue(className)) { qualifiedName = className; } if (qualifiedName != null) { try { /* * TODO - the map right now contains just class names. if we * use a map of classname/class then we don't have to hit * the class loader for every type - much faster */ myType = pmdClassLoader.loadClass(qualifiedName); } catch (ClassNotFoundException e) { myType = processOnDemand(qualifiedName); } catch (NoClassDefFoundError e) { myType = processOnDemand(qualifiedName); } catch (LinkageError e) { myType = processOnDemand(qualifiedName); } } } if (myType == null && qualifiedName != null && qualifiedName.contains(".")) { // try if the last part defines a inner class String qualifiedNameInner = qualifiedName.substring(0, qualifiedName.lastIndexOf('.')) + "$" + qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1); try { myType = pmdClassLoader.loadClass(qualifiedNameInner); } catch (Exception e) { // ignored } } if (myType == null && qualifiedName != null && !qualifiedName.contains(".")) { // try again with java.lang.... try { myType = pmdClassLoader.loadClass("java.lang." + qualifiedName); } catch (Exception e) { // ignored } } if (myType != null) { node.setType(myType); } } /** * Check whether the supplied class name exists. */ public boolean classNameExists(String fullyQualifiedClassName) { try { pmdClassLoader.loadClass(fullyQualifiedClassName); return true; // Class found } catch (ClassNotFoundException e) { return false; } catch (NoClassDefFoundError e) { return false; } } public Class<?> loadClass(String fullyQualifiedClassName) { try { return pmdClassLoader.loadClass(fullyQualifiedClassName); } catch (ClassNotFoundException e) { return null; } } private Class<?> processOnDemand(String qualifiedName) { for (String entry : importedOnDemand) { try { return pmdClassLoader.loadClass(entry + "." + qualifiedName); } catch (Throwable e) { } } return null; } private String getClassName(ASTCompilationUnit node) { ASTClassOrInterfaceDeclaration classDecl = node.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class); if (classDecl == null) { // Happens if this compilation unit only contains an enum return null; } if (node.declarationsAreInDefaultPackage()) { return classDecl.getImage(); } ASTPackageDeclaration pkgDecl = node.getPackageDeclaration(); importedOnDemand.add(pkgDecl.getPackageNameImage()); return pkgDecl.getPackageNameImage() + "." + classDecl.getImage(); } /** * If the outer class wasn't found then we'll get in here * * @param node */ private void populateImports(ASTCompilationUnit node) { List<ASTImportDeclaration> theImportDeclarations = node.findChildrenOfType(ASTImportDeclaration.class); importedClasses.putAll(JAVA_LANG); // go through the imports for (ASTImportDeclaration anImportDeclaration : theImportDeclarations) { String strPackage = anImportDeclaration.getPackageName(); if (anImportDeclaration.isImportOnDemand()) { importedOnDemand.add(strPackage); } else if (!anImportDeclaration.isImportOnDemand()) { String strName = anImportDeclaration.getImportedName(); importedClasses.put(strName, strName); importedClasses.put(strName.substring(strPackage.length() + 1), strName); } } } private void populateClassName(ASTCompilationUnit node, String className) throws ClassNotFoundException { node.setType(pmdClassLoader.loadClass(className)); importedClasses.putAll(pmdClassLoader.getImportedClasses(className)); } }