Merge branch 'pr-2380'

[java] Make TypeHelper use the class loading cache of ClassTypeResolver #2380
This commit is contained in:
Andreas Dangel
2020-04-03 15:41:09 +02:00
10 changed files with 269 additions and 34 deletions

View File

@ -60,6 +60,11 @@ public class ASTCompilationUnit extends AbstractJavaTypeNode implements RootNode
return null;
}
@Override
public ASTCompilationUnit getRoot() {
return this;
}
/**
* Returns the package name of this compilation unit. If this is in
* the default package, returns the empty string.

View File

@ -15,6 +15,7 @@ public abstract class AbstractJavaNode extends AbstractJjtreeNode<JavaNode> impl
protected JavaParser parser;
private Scope scope;
private Comment comment;
private ASTCompilationUnit root;
@InternalApi
@Deprecated
@ -64,6 +65,14 @@ public abstract class AbstractJavaNode extends AbstractJjtreeNode<JavaNode> impl
return data;
}
@Override
public ASTCompilationUnit getRoot() {
if (root == null) {
root = getParent().getRoot();
}
return root;
}
@Override
public Scope getScope() {
if (scope == null) {

View File

@ -41,6 +41,7 @@ public interface JavaNode extends ScopedNode {
@Deprecated
Object childrenAccept(JavaParserVisitor visitor, Object data);
ASTCompilationUnit getRoot();
@Override
JavaNode getChild(int index);

View File

@ -66,7 +66,7 @@ public class DuplicateImportsRule extends AbstractJavaRule {
return true;
}
} else {
Class<?> importClass = node.getClassTypeResolver().loadClass(thisImportOnDemand.getName());
Class<?> importClass = node.getClassTypeResolver().loadClassOrNull(thisImportOnDemand.getName());
if (importClass != null) {
for (Method m : importClass.getMethods()) {
if (Modifier.isStatic(m.getModifiers()) && m.getName().equals(singleTypeName)) {

View File

@ -99,6 +99,7 @@ import net.sourceforge.pmd.lang.java.ast.JavaParserVisitorAdapter;
import net.sourceforge.pmd.lang.java.ast.TypeNode;
import net.sourceforge.pmd.lang.java.symboltable.ClassScope;
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
import net.sourceforge.pmd.lang.java.typeresolution.typedefinition.JavaTypeDefinition;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.Scope;
@ -112,7 +113,7 @@ import net.sourceforge.pmd.lang.symboltable.Scope;
@Deprecated
@InternalApi
public class ClassTypeResolver extends JavaParserVisitorAdapter {
public class ClassTypeResolver extends JavaParserVisitorAdapter implements NullableClassLoader {
private static final Logger LOG = Logger.getLogger(ClassTypeResolver.class.getName());
@ -1487,10 +1488,15 @@ public class ClassTypeResolver extends JavaParserVisitorAdapter {
return pmdClassLoader.loadClassOrNull(fullyQualifiedClassName) != null;
}
public Class<?> loadClass(String fullyQualifiedClassName) {
@Override
public Class<?> loadClassOrNull(String fullyQualifiedClassName) {
return pmdClassLoader.loadClassOrNull(fullyQualifiedClassName);
}
public Class<?> loadClass(String fullyQualifiedClassName) {
return loadClassOrNull(fullyQualifiedClassName);
}
private Class<?> processOnDemand(String qualifiedName) {
for (String entry : importedOnDemand) {
String fullClassName = entry + "." + qualifiedName;
@ -1533,12 +1539,12 @@ public class ClassTypeResolver extends JavaParserVisitorAdapter {
String strPackage = anImportDeclaration.getPackageName();
if (anImportDeclaration.isStatic()) {
if (anImportDeclaration.isImportOnDemand()) {
importOnDemandStaticClasses.add(JavaTypeDefinition.forClass(loadClass(strPackage)));
importOnDemandStaticClasses.add(JavaTypeDefinition.forClass(loadClassOrNull(strPackage)));
} else { // not import on-demand
String strName = anImportDeclaration.getImportedName();
String fieldName = strName.substring(strName.lastIndexOf('.') + 1);
Class<?> staticClassWithField = loadClass(strPackage);
Class<?> staticClassWithField = loadClassOrNull(strPackage);
if (staticClassWithField != null) {
JavaTypeDefinition typeDef = getFieldType(JavaTypeDefinition.forClass(staticClassWithField),
fieldName, currentAcu.getType());

View File

@ -15,6 +15,7 @@ import java.util.concurrent.ConcurrentMap;
import org.objectweb.asm.ClassReader;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
import net.sourceforge.pmd.lang.java.typeresolution.visitors.PMDASMVisitor;
/*
@ -36,7 +37,7 @@ import net.sourceforge.pmd.lang.java.typeresolution.visitors.PMDASMVisitor;
*/
@InternalApi
@Deprecated
public final class PMDASMClassLoader extends ClassLoader {
public final class PMDASMClassLoader extends ClassLoader implements NullableClassLoader {
private static PMDASMClassLoader cachedPMDASMClassLoader;
private static ClassLoader cachedClassLoader;
@ -81,6 +82,7 @@ public final class PMDASMClassLoader extends ClassLoader {
* Not throwing CNFEs to represent failure makes a huge performance
* difference. Typeres as a whole is 2x faster.
*/
@Override
public Class<?> loadClassOrNull(String name) {
if (dontBother.containsKey(name)) {
return null;

View File

@ -4,7 +4,12 @@
package net.sourceforge.pmd.lang.java.typeresolution;
import org.apache.commons.lang3.ClassUtils;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import net.sourceforge.pmd.lang.java.ast.ASTAnnotationTypeDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
@ -13,9 +18,27 @@ import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTImplementsList;
import net.sourceforge.pmd.lang.java.ast.TypeNode;
import net.sourceforge.pmd.lang.java.symboltable.TypedNameDeclaration;
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader.ClassLoaderWrapper;
public final class TypeHelper {
/** Maps names of primitives to their corresponding primitive {@code Class}es. */
private static final Map<String, Class<?>> PRIMITIVES_BY_NAME = new HashMap<>();
static {
PRIMITIVES_BY_NAME.put("boolean", Boolean.TYPE);
PRIMITIVES_BY_NAME.put("byte", Byte.TYPE);
PRIMITIVES_BY_NAME.put("char", Character.TYPE);
PRIMITIVES_BY_NAME.put("short", Short.TYPE);
PRIMITIVES_BY_NAME.put("int", Integer.TYPE);
PRIMITIVES_BY_NAME.put("long", Long.TYPE);
PRIMITIVES_BY_NAME.put("double", Double.TYPE);
PRIMITIVES_BY_NAME.put("float", Float.TYPE);
PRIMITIVES_BY_NAME.put("void", Void.TYPE);
}
private TypeHelper() {
// utility class
}
@ -34,6 +57,10 @@ public final class TypeHelper {
* @return <code>true</code> if type node n is of type clazzName or a subtype of clazzName
*/
public static boolean isA(final TypeNode n, final String clazzName) {
if (n.getType() != null && n.getType().isAnnotation()) {
return isAnnotationSubtype(n.getType(), clazzName);
}
final Class<?> clazz = loadClassWithNodeClassloader(n, clazzName);
if (clazz != null || n.getType() != null) {
@ -43,6 +70,20 @@ public final class TypeHelper {
return fallbackIsA(n, clazzName);
}
/**
* Returns true if the class n is a subtype of clazzName, given n
* is an annotationt type.
*/
private static boolean isAnnotationSubtype(Class<?> n, String clazzName) {
assert n != null && n.isAnnotation() : "Not an annotation type";
// then, the supertype may only be Object, j.l.Annotation, or the class name
// this avoids classloading altogether
// this is used e.g. by the typeIs function in XPath
return "java.lang.annotation.Annotation".equals(clazzName)
|| "java.lang.Object".equals(clazzName)
|| clazzName.equals(n.getName());
}
private static boolean fallbackIsA(TypeNode n, String clazzName) {
if (clazzName.equals(n.getImage()) || clazzName.endsWith("." + n.getImage())) {
return true;
@ -73,9 +114,11 @@ public final class TypeHelper {
return "java.lang.Enum".equals(clazzName)
// supertypes of Enum
|| "java.lang.Comparable".equals(clazzName)
|| "java.io.Serializable".equals(clazzName);
|| "java.io.Serializable".equals(clazzName)
|| "java.lang.Object".equals(clazzName);
} else if (n instanceof ASTAnnotationTypeDeclaration) {
return "java.lang.annotation.Annotation".equals(clazzName);
return "java.lang.annotation.Annotation".equals(clazzName)
|| "java.lang.Object".equals(clazzName);
}
return false;
@ -90,6 +133,11 @@ public final class TypeHelper {
* @return <code>true</code> if type node n is exactly of type clazzName.
*/
public static boolean isExactlyA(final TypeNode n, final String clazzName) {
if (n.getType() != null && n.getType().getName().equals(clazzName)) {
// fast path avoiding classloading
return true;
}
final Class<?> clazz = loadClassWithNodeClassloader(n, clazzName);
if (clazz != null) {
@ -101,34 +149,87 @@ public final class TypeHelper {
private static Class<?> loadClassWithNodeClassloader(final TypeNode n, final String clazzName) {
if (n.getType() != null) {
return loadClass(n.getType().getClassLoader(), clazzName);
return loadClass(n.getRoot().getClassTypeResolver(), clazzName);
}
return null;
}
private static Class<?> loadClass(final ClassLoader nullableClassLoader, final String clazzName) {
try {
ClassLoader classLoader = nullableClassLoader;
if (classLoader == null) {
// Using the system classloader then
classLoader = ClassLoader.getSystemClassLoader();
/**
* Load a class. Supports loading array types like 'java.lang.String[]' and
* converting a canonical name to a binary name (eg 'java.util.Map.Entry' ->
* 'java.util.Map$Entry').
*/
// test only
static Class<?> loadClass(NullableClassLoader ctr, String className) {
return loadClassMaybeArray(ctr, StringUtils.deleteWhitespace(className));
}
private static Class<?> loadClassFromCanonicalName(NullableClassLoader ctr, String className) {
Class<?> clazz = PRIMITIVES_BY_NAME.get(className);
if (clazz == null) {
clazz = ctr.loadClassOrNull(className);
}
if (clazz != null) {
return clazz;
}
// allow path separators (.) as inner class name separators
final int lastDotIndex = className.lastIndexOf('.');
if (lastDotIndex >= 0) {
String asInner = className.substring(0, lastDotIndex)
+ '$' + className.substring(lastDotIndex + 1);
return loadClassFromCanonicalName(ctr, asInner);
}
return null;
}
private static Class<?> loadClassMaybeArray(NullableClassLoader classLoader,
String className) {
Validate.notNull(className, "className must not be null.");
if (className.endsWith("[]")) {
int dimension = 0;
int i = className.length();
while (i >= 2 && className.startsWith("[]", i - 2)) {
dimension++;
i -= 2;
}
// If the requested type is in the classpath, using the same classloader should work
return ClassUtils.getClass(classLoader, clazzName);
} catch (ClassNotFoundException ignored) {
// The requested type is not on the auxclasspath. This might happen, if the type node
// is probed for a specific type (e.g. is is a JUnit5 Test Annotation class).
// Failing to resolve clazzName does not necessarily indicate an incomplete auxclasspath.
} catch (final LinkageError expected) {
// We found the class but it's invalid / incomplete. This may be an incomplete auxclasspath
// if it was a NoClassDefFoundError. TODO : Report it?
checkJavaIdent(className, i);
String elementName = className.substring(0, i);
Class<?> elementType = loadClassFromCanonicalName(classLoader, elementName);
if (elementType == null) {
return null;
}
return Array.newInstance(elementType, (int[]) Array.newInstance(int.class, dimension)).getClass();
} else {
checkJavaIdent(className, className.length());
return loadClassFromCanonicalName(classLoader, className);
}
}
private static IllegalArgumentException invalidClassName(String className) {
return new IllegalArgumentException("Not a valid class name \"" + className + "\"");
}
private static void checkJavaIdent(String className, int endOffsetExclusive) {
if (endOffsetExclusive <= 0 || !Character.isJavaIdentifierStart(className.charAt(0))) {
throw invalidClassName(className);
}
return null;
for (int i = 1; i < endOffsetExclusive; i++) {
char c = className.charAt(i);
if (!(Character.isJavaIdentifierPart(c) || c == '.')) {
throw invalidClassName(className);
}
}
}
/** @see #isA(TypeNode, String) */
public static boolean isA(TypeNode n, Class<?> clazz) {
return subclasses(n, clazz);
@ -142,7 +243,7 @@ public final class TypeHelper {
Class<?> type = vnd.getType();
for (final Class<?> clazz : clazzes) {
if (type != null && type.equals(clazz) || type == null
&& (clazz.getSimpleName().equals(vnd.getTypeImage()) || clazz.getName().equals(vnd.getTypeImage()))) {
&& (clazz.getSimpleName().equals(vnd.getTypeImage()) || clazz.getName().equals(vnd.getTypeImage()))) {
return true;
}
}
@ -192,7 +293,7 @@ public final class TypeHelper {
public static boolean isA(TypedNameDeclaration vnd, String className) {
Class<?> type = vnd.getType();
if (type != null) {
Class<?> clazz = loadClass(type.getClassLoader(), className);
Class<?> clazz = loadClass(ClassLoaderWrapper.wrapNullable(type.getClassLoader()), className);
if (clazz != null) {
return clazz.isAssignableFrom(type);
}

View File

@ -0,0 +1,49 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.typeresolution.internal;
import net.sourceforge.pmd.lang.java.typeresolution.PMDASMClassLoader;
/**
* A classloader that doesn't throw a {@link ClassNotFoundException}
* to report unresolved classes. This is a big performance improvement
* for {@link PMDASMClassLoader}, which caches negative cases.
*
* <p>See https://github.com/pmd/pmd/pull/2236
*/
public interface NullableClassLoader {
/**
* Load a class from its binary name. Returns null if not found.
*/
Class<?> loadClassOrNull(String binaryName);
class ClassLoaderWrapper implements NullableClassLoader {
private final ClassLoader classLoader;
private ClassLoaderWrapper(ClassLoader classLoader) {
assert classLoader != null : "Null classloader";
this.classLoader = classLoader;
}
@Override
public Class<?> loadClassOrNull(String binaryName) {
try {
return classLoader.loadClass(binaryName);
} catch (ClassNotFoundException e) {
return null;
}
}
public static ClassLoaderWrapper wrapNullable(ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
return new ClassLoaderWrapper(classLoader);
}
}
}

View File

@ -4,19 +4,30 @@
package net.sourceforge.pmd.lang.java.typeresolution;
import static net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader.ClassLoaderWrapper.wrapNullable;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.Map;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import net.sourceforge.pmd.lang.java.ast.ASTAnnotationTypeDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration;
import net.sourceforge.pmd.lang.java.ast.TypeNode;
import net.sourceforge.pmd.lang.java.symboltable.BaseNonParserTest;
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
public class TypeHelperTest extends BaseNonParserTest {
private static final NullableClassLoader LOADER = wrapNullable(TypeHelperTest.class.getClassLoader());
@Rule
public final ExpectedException expect = ExpectedException.none();
@Test
public void testIsAFallback() {
@ -46,11 +57,10 @@ public class TypeHelperTest extends BaseNonParserTest {
Assert.assertNull(klass.getType());
Assert.assertTrue(TypeHelper.isA(klass, "org.FooBar"));
Assert.assertTrue(TypeHelper.isA(klass, "java.lang.Iterable"));
Assert.assertTrue(TypeHelper.isA(klass, Iterable.class));
Assert.assertTrue(TypeHelper.isA(klass, Enum.class));
Assert.assertTrue(TypeHelper.isA(klass, Serializable.class));
Assert.assertTrue(TypeHelper.isA(klass, Comparable.class));
assertIsA(klass, Iterable.class);
assertIsA(klass, Enum.class);
assertIsA(klass, Serializable.class);
assertIsA(klass, Object.class);
}
@Test
@ -64,8 +74,58 @@ public class TypeHelperTest extends BaseNonParserTest {
Assert.assertNull(klass.getType());
Assert.assertTrue(TypeHelper.isA(klass, "org.FooBar"));
Assert.assertTrue(TypeHelper.isA(klass, Annotation.class));
assertIsA(klass, Annotation.class);
assertIsA(klass, Object.class);
}
private void assertIsA(TypeNode node, Class<?> type) {
Assert.assertTrue("TypeHelper::isA with class arg: " + type.getCanonicalName(),
TypeHelper.isA(node, type));
Assert.assertTrue("TypeHelper::isA with string arg: " + type.getCanonicalName(),
TypeHelper.isA(node, type.getCanonicalName()));
}
private void assertIsExactlyA(TypeNode node, Class<?> type) {
Assert.assertTrue(TypeHelper.isExactlyA(node, type.getCanonicalName()));
assertIsA(node, type);
}
@Test
public void testNestedClass() {
Class<?> c = TypeHelper.loadClass(LOADER, "java.util.Map.Entry");
Assert.assertEquals(Map.Entry.class, c);
}
@Test
public void testPrimitiveArray() {
Class<?> c = TypeHelper.loadClass(LOADER, "int[ ]");
Assert.assertEquals(int[].class, c);
}
@Test
public void testNestedClassArray() {
Class<?> c = TypeHelper.loadClass(LOADER, "java.util.Map.Entry[ ]");
Assert.assertEquals(Map.Entry[].class, c);
}
@Test
public void testInvalidName() {
expect.expect(IllegalArgumentException.class);
TypeHelper.loadClass(LOADER, "java.util.Map ]");
}
@Test
public void testInvalidName2() {
expect.expect(IllegalArgumentException.class);
TypeHelper.loadClass(LOADER, "[]");
}
@Test
public void testNullName() {
expect.expect(NullPointerException.class);
TypeHelper.loadClass(LOADER, null);
}
}