/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.symboltable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import net.sourceforge.pmd.lang.java.typeresolution.PMDASMClassLoader; import net.sourceforge.pmd.util.ClasspathClassLoader; /** * Keeps track of the types encountered in a ASTCompilationUnit */ public class TypeSet { private final PMDASMClassLoader pmdClassLoader; private boolean hasAuxclasspath; private String pkg; private Set<String> imports = new HashSet<>(); private List<Resolver> resolvers = new ArrayList<>(); /** * The {@link TypeSet} provides type resolution for the symbol facade. */ public TypeSet() { this(TypeSet.class.getClassLoader()); } /** * The {@link TypeSet} provides type resolution for the symbol facade. * * @param classLoader * the class loader to use to search classes (could be an * auxiliary class path) */ public TypeSet(ClassLoader classLoader) { ClassLoader cl = classLoader; if (cl == null) { cl = TypeSet.class.getClassLoader(); } hasAuxclasspath = cl instanceof ClasspathClassLoader; pmdClassLoader = PMDASMClassLoader.getInstance(cl); } /** * Whether the classloader is using the auxclasspath or not. * * @return <code>true</code> if the classloader is using the auxclasspath * feature */ public boolean hasAuxclasspath() { return hasAuxclasspath; } /** * A resolver that can resolve a class by name. The name can be a simple * name or a fully qualified name. */ // TODO should Resolver provide a canResolve() and a resolve()? Requiring 2 // calls seems clunky... but so does this throwing an exception for flow // control... public interface Resolver { /** * Resolve the class by the given name * * @param name * the name of the class, might be fully classified or not. * @return the class * @throws ClassNotFoundException * if the class couldn't be found */ Class<?> resolve(String name) throws ClassNotFoundException; /** * Checks if the given class could be resolved by this resolver. Notice, * that a resolver's ability to resolve a class does not imply that the * class will actually be found and resolved. * * @param name * the name of the class, might be fully classified or not. * @return whether the class can be resolved */ boolean couldResolve(String name); } /** * Base Resolver class that support a {@link PMDASMClassLoader} class * loader. */ public abstract static class AbstractResolver implements Resolver { /** the class loader. */ protected final PMDASMClassLoader pmdClassLoader; private final Map<String, String> classNames; /** * Creates a new AbstractResolver that uses the given class loader. * * @param pmdClassLoader * the class loader to use */ public AbstractResolver(final PMDASMClassLoader pmdClassLoader) { this.pmdClassLoader = pmdClassLoader; classNames = new HashMap<>(); } /** * Resolves the given class name with the given FQCN, considering it may * be an inner class. * * @param name * The name of the class to load. * @param fqName * The proposed FQCN for the class. * @return The matched class or null if not found. */ protected Class<?> resolveMaybeInner(final String name, final String fqName) { // Do we know the actual class name? final String className = classNames.get(name); if (className != null) { try { return pmdClassLoader.loadClass(className); } catch (final ClassNotFoundException e) { // Ignored, can never actually happen } } if (fqName != null) { final StringBuilder sb = new StringBuilder(fqName); String actualClassName = fqName; // We have a FQCN, but it may be an inner class, so we have to // brute force our way... do { if (pmdClassLoader.couldResolve(actualClassName)) { try { final Class<?> c = pmdClassLoader.loadClass(actualClassName); // Update the mapping classNames.put(name, actualClassName); return c; } catch (final ClassNotFoundException e) { // Ignored } } // Check if the last segment is an inner class final int lastDot = actualClassName.lastIndexOf('.'); if (lastDot == -1) { break; } sb.setCharAt(lastDot, '$'); actualClassName = sb.toString(); } while (true); } return null; } public boolean couldResolve(final String name) { /* * Resolvers based on this one, will attempt to load the class from * the class loader, so ask him */ return classNames.containsKey(name) || pmdClassLoader.couldResolve(name); } } /** * Resolver that tries to resolve the given simple class name with the * explicit import statements. */ public static class ExplicitImportResolver extends AbstractResolver { private Map<String, String> importStmts; /** * Creates a new {@link ExplicitImportResolver}. * * @param pmdClassLoader * the class loader to use. * @param importStmts * the import statements */ public ExplicitImportResolver(PMDASMClassLoader pmdClassLoader, Set<String> importStmts) { super(pmdClassLoader); // unfold imports, to store both FQ and unqualified names mapped to // the FQ name this.importStmts = new HashMap<>(); for (final String stmt : importStmts) { if (stmt.endsWith("*")) { continue; } this.importStmts.put(stmt, stmt); final int lastDotIdx = stmt.lastIndexOf('.'); if (lastDotIdx != -1) { this.importStmts.put(stmt.substring(lastDotIdx + 1), stmt); } } } @Override public Class<?> resolve(final String name) throws ClassNotFoundException { final Class<?> c = resolveMaybeInner(name, importStmts.get(name)); if (c == null) { throw new ClassNotFoundException("Type " + name + " not found"); } return c; } @Override public boolean couldResolve(final String name) { return importStmts.containsKey(name); } } /** * Resolver that uses the current package to resolve a simple class name. */ public static class CurrentPackageResolver extends AbstractResolver { private final String pkg; /** * Creates a new {@link CurrentPackageResolver} * * @param pmdClassLoader * the class loader to use * @param pkg * the package name */ public CurrentPackageResolver(PMDASMClassLoader pmdClassLoader, String pkg) { super(pmdClassLoader); if (pkg == null || pkg.length() == 0) { this.pkg = null; } else { this.pkg = pkg + "."; } } @Override public Class<?> resolve(String name) throws ClassNotFoundException { if (name == null) { throw new ClassNotFoundException(); } return pmdClassLoader.loadClass(qualifyName(name)); } @Override public boolean couldResolve(String name) { return pmdClassLoader.couldResolve(qualifyName(name)); } private String qualifyName(final String name) { final String qualifiedName = name.replace('.', '$'); if (pkg == null) { return qualifiedName; } /* * String.concat is bad in general, but for simple 2 string concatenation, it's the fastest * See http://www.rationaljava.com/2015/02/the-optimum-method-to-concatenate.html */ return pkg.concat(qualifiedName); } } /** * Resolver that resolves simple class names from the implicit import of * <code>java.lang.*</code>. */ // TODO cite the JLS section on implicit imports public static class ImplicitImportResolver extends AbstractResolver { /* * They aren't so many to bother about memory, but are used all the * time, so we worry about performance. On average, you can expect this * cache to have ~90% hit ratio unless abusing star imports (import on * demand) */ private static final ConcurrentHashMap<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>(); /** * Creates a {@link ImplicitImportResolver} * * @param pmdClassLoader * the class loader */ public ImplicitImportResolver(PMDASMClassLoader pmdClassLoader) { super(pmdClassLoader); } @Override public Class<?> resolve(String name) throws ClassNotFoundException { if (name == null) { throw new ClassNotFoundException(); } Class<?> clazz = CLASS_CACHE.get(name); if (clazz != null) { return clazz; } /* * String.concat is bad in general, but for simple 2 string concatenation, it's the fastest * See http://www.rationaljava.com/2015/02/the-optimum-method-to-concatenate.html */ clazz = pmdClassLoader.loadClass("java.lang.".concat(name.replace('.', '$'))); CLASS_CACHE.putIfAbsent(name, clazz); return clazz; } @Override public boolean couldResolve(String name) { /* * String.concat is bad in general, but for simple 2 string concatenation, it's the fastest * See http://www.rationaljava.com/2015/02/the-optimum-method-to-concatenate.html */ return pmdClassLoader.couldResolve("java.lang.".concat(name.replace('.', '$'))); } } /** * Resolver that uses the "on demand" import statements. */ public static class ImportOnDemandResolver extends AbstractResolver { private Set<String> importStmts; /** * Creates a {@link ImportOnDemandResolver} * * @param pmdClassLoader * the class loader to use * @param importStmts * the import statements */ public ImportOnDemandResolver(PMDASMClassLoader pmdClassLoader, Set<String> importStmts) { super(pmdClassLoader); this.importStmts = new HashSet<>(); for (final String stmt : importStmts) { if (stmt.endsWith("*")) { this.importStmts.add(stmt); } } } @Override public Class<?> resolve(String name) throws ClassNotFoundException { if (name == null) { throw new ClassNotFoundException(); } name = name.replace('.', '$'); for (String importStmt : importStmts) { final String fqClassName = new StringBuilder(importStmt.length() + name.length()).append(importStmt) .replace(importStmt.length() - 1, importStmt.length(), name).toString(); if (pmdClassLoader.couldResolve(fqClassName)) { try { return pmdClassLoader.loadClass(fqClassName); } catch (ClassNotFoundException e) { // ignored } } } throw new ClassNotFoundException("Type " + name + " not found"); } @Override public boolean couldResolve(String name) { name = name.replace('.', '$'); for (String importStmt : importStmts) { final String fqClassName = new StringBuilder(importStmt.length() + name.length()).append(importStmt) .replace(importStmt.length() - 1, importStmt.length(), name).toString(); // can any class be resolved / was never attempted? if (pmdClassLoader.couldResolve(fqClassName)) { return true; } } return false; } } /** * Resolver that resolves primitive types such as int or double. */ public static class PrimitiveTypeResolver implements Resolver { private static final Map<String, Class<?>> PRIMITIVE_TYPES; static { final Map<String, Class<?>> types = new HashMap<>(); types.put("int", int.class); types.put("float", float.class); types.put("double", double.class); types.put("long", long.class); types.put("boolean", boolean.class); types.put("byte", byte.class); types.put("short", short.class); types.put("char", char.class); PRIMITIVE_TYPES = Collections.unmodifiableMap(types); } @Override public Class<?> resolve(String name) throws ClassNotFoundException { if (!PRIMITIVE_TYPES.containsKey(name)) { throw new ClassNotFoundException(name); } return PRIMITIVE_TYPES.get(name); } @Override public boolean couldResolve(String name) { return PRIMITIVE_TYPES.containsKey(name); } } /** * Resolver that resolves the "void" type. */ public static class VoidResolver implements Resolver { @Override public Class<?> resolve(String name) throws ClassNotFoundException { if ("void".equals(name)) { return void.class; } throw new ClassNotFoundException(name); } @Override public boolean couldResolve(String name) { return "void".equals(name); } } /** * Resolver that simply loads the class by name. This only works if the * class name is given as a fully qualified name. */ public static class FullyQualifiedNameResolver extends AbstractResolver { /** * Creates a {@link FullyQualifiedNameResolver} * * @param pmdClassLoader * the class loader to use */ public FullyQualifiedNameResolver(PMDASMClassLoader pmdClassLoader) { super(pmdClassLoader); } @Override public Class<?> resolve(String name) throws ClassNotFoundException { if (name == null) { throw new ClassNotFoundException(); } final Class<?> c = resolveMaybeInner(name, name); if (c == null) { throw new ClassNotFoundException("Type " + name + " not found"); } return c; } @Override public boolean couldResolve(String name) { /* * We can always try! * If a file used an explicit import on A.Inner, the class loader will register * A.Inner can't be resolved even if A$Inner can. * If a second file used A.Inner without an explicit import, we would end here, * super.couldResolve("A.Inner") will return false, but we CAN resolve it as A$Inner. */ return true; } } public void setASTCompilationUnitPackage(String pkg) { this.pkg = pkg; } public String getASTCompilationUnitPackage() { return pkg; } /** * Adds a import to the list of imports * * @param importString * the import to add */ public void addImport(String importString) { imports.add(importString); } public int getImportsCount() { return imports.size(); } public Set<String> getExplicitImports() { return imports; } /** * Resolves a class by its name using all known resolvers. * * @param name * the name of the class, can be a simple name or a fully * qualified name. * @return the class or <code>null</code> if none found */ public Class<?> findClass(String name) { // we don't build the resolvers until now since we first want to get all // the imports if (resolvers.isEmpty()) { buildResolvers(); } for (final Resolver resolver : resolvers) { if (resolver.couldResolve(name)) { try { return resolver.resolve(name); } catch (ClassNotFoundException cnfe) { // ignored, maybe another resolver will find the class } } } return null; } private void buildResolvers() { resolvers.add(new PrimitiveTypeResolver()); resolvers.add(new VoidResolver()); resolvers.add(new ExplicitImportResolver(pmdClassLoader, imports)); resolvers.add(new CurrentPackageResolver(pmdClassLoader, pkg)); resolvers.add(new ImplicitImportResolver(pmdClassLoader)); resolvers.add(new ImportOnDemandResolver(pmdClassLoader, imports)); resolvers.add(new FullyQualifiedNameResolver(pmdClassLoader)); } }