From 84e735047133cfd4d4e40159040d6b9e1041533f Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 4 Mar 2024 13:30:56 +0100 Subject: [PATCH] improve Index[View].getKnownUsers() Previously, the `getKnownUsers()` method only considered a class as used in another class when it occured in the list of class references in the other class's constant pool. With this commit, a class is also considered as used in another class when it occurs in the other class's signature (superclass type, superinterface types, type parameters), in the signatures of the other class's methods (return type, parameter types, exception types, type parameters), or in the types of the other class's fields and record components. --- .../main/java/org/jboss/jandex/IndexView.java | 39 ++++++- .../main/java/org/jboss/jandex/Indexer.java | 87 ++++++++++++-- .../org/jboss/jandex/test/KnownUsersTest.java | 110 ++++++++++++++++++ 3 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 core/src/test/java/org/jboss/jandex/test/KnownUsersTest.java diff --git a/core/src/main/java/org/jboss/jandex/IndexView.java b/core/src/main/java/org/jboss/jandex/IndexView.java index 3bc850ff..8751d597 100644 --- a/core/src/main/java/org/jboss/jandex/IndexView.java +++ b/core/src/main/java/org/jboss/jandex/IndexView.java @@ -452,8 +452,17 @@ default ModuleInfo getModuleByName(String moduleName) { } /** - * Obtains a list of classes that use the specified class. In other words, a list of classes that include - * a reference to the specified class in their constant pool. + * Returns a list of classes in this index that use the specified class. For one class + * to use another class, the other class has to: + * * * @param className the name of the class to look for * @return a non-null list of classes that use the specified class @@ -461,8 +470,17 @@ default ModuleInfo getModuleByName(String moduleName) { Collection getKnownUsers(DotName className); /** - * Obtains a list of classes that use the specified class. In other words, a list of classes that include - * a reference to the specified class in their constant pool. + * Returns a list of classes in this index that use the specified class. For one class + * to use another class, the other class has to: + * * * @param className the name of the class to look for * @return a non-null list of classes that use the specified class @@ -472,8 +490,17 @@ default Collection getKnownUsers(String className) { } /** - * Obtains a list of classes that use the specified class. In other words, a list of classes that include - * a reference to the specified class in their constant pool. + * Returns a list of classes in this index that use the specified class. For one class + * to use another class, the other class has to: + *
    + *
  • occur in the signature of the class (that is, in the superclass type, + * in the superinterface types, or in the type parameters), or
  • + *
  • occur in the signature of any of the class's methods (that is, in the return type, + * in the parameter types, in the exception types, or in the type parameters), or
  • + *
  • occur in the type of any of the class's fields or record components, or
  • + *
  • occur in the list of class references in the constant pool of the class, + * as described by the JLS and JVMS.
  • + *
* * @param clazz the class to look for * @return a non-null list of classes that use the specified class diff --git a/core/src/main/java/org/jboss/jandex/Indexer.java b/core/src/main/java/org/jboss/jandex/Indexer.java index 93d9e625..2b219931 100644 --- a/core/src/main/java/org/jboss/jandex/Indexer.java +++ b/core/src/main/java/org/jboss/jandex/Indexer.java @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -407,7 +408,7 @@ void returnConstantAnnoAttributes(byte[] attributes) { private Map> implementors; private Map classes; private Map modules; - private Map> users; + private Map> users; // must be a linked set for reproducibility private NameTable names; private GenericSignatureParser signatureParser; private final TmpObjects tmpObjects = new TmpObjects(); @@ -432,7 +433,7 @@ private void initIndexMaps() { modules = new HashMap(); if (users == null) - users = new HashMap>(); + users = new HashMap>(); if (names == null) names = new NameTable(); @@ -1082,6 +1083,7 @@ private void resolveTypeAnnotations() { } private void resolveUsers() throws IOException { + // class references in constant pool int poolSize = constantPoolSize; byte[] pool = constantPool; int[] offsets = constantPoolOffsets; @@ -1091,16 +1093,79 @@ private void resolveUsers() throws IOException { if (pool[offset] == CONSTANT_CLASS) { int nameIndex = (pool[++offset] & 0xFF) << 8 | (pool[++offset] & 0xFF); DotName usedClass = names.convertToName(decodeUtf8Entry(nameIndex), '/'); - List usersOfClass = users.get(usedClass); - if (usersOfClass == null) { - usersOfClass = new ArrayList(); - users.put(usedClass, usersOfClass); - } - usersOfClass.add(this.currentClass); + recordUsedClass(usedClass); + } + } + + // class declaration + for (TypeVariable typeParameter : currentClass.typeParameters()) { + recordUsedType(typeParameter); + } + recordUsedType(currentClass.superClassType()); + for (Type interfaceType : currentClass.interfaceTypes()) { + recordUsedType(interfaceType); + } + // field declarations + for (FieldInfo field : fields) { + recordUsedType(field.type()); + } + // method declarations (ignoring receiver types, they are always the current class) + for (MethodInfo method : methods) { + for (TypeVariable typeParameter : method.typeParameters()) { + recordUsedType(typeParameter); + } + recordUsedType(method.returnType()); + for (Type parameterType : method.parameterTypes()) { + recordUsedType(parameterType); } + for (Type exceptionType : method.exceptions()) { + recordUsedType(exceptionType); + } + } + // record component declarations + for (RecordComponentInfo recordComponent : recordComponents) { + recordUsedType(recordComponent.type()); + } + } + + private void recordUsedType(Type type) { + if (type == null) { + return; + } + + switch (type.kind()) { + case CLASS: + recordUsedClass(type.asClassType().name()); + break; + case PARAMETERIZED_TYPE: + recordUsedClass(type.asParameterizedType().name()); + for (Type typeArgument : type.asParameterizedType().arguments()) { + recordUsedType(typeArgument); + } + break; + case ARRAY: + recordUsedType(type.asArrayType().elementType()); + break; + case WILDCARD_TYPE: + recordUsedType(type.asWildcardType().bound()); + break; + case TYPE_VARIABLE: + for (Type bound : type.asTypeVariable().boundArray()) { + recordUsedType(bound); + } + break; } } + private void recordUsedClass(DotName usedClass) { + Set usersOfClass = users.get(usedClass); + if (usersOfClass == null) { + usersOfClass = new LinkedHashSet<>(); + users.put(usedClass, usersOfClass); + } + usersOfClass.add(this.currentClass); + } + private void updateTypeTargets() { for (Map.Entry> entry : typeAnnotations.entrySet()) { AnnotationTarget key = entry.getKey(); @@ -2557,7 +2622,11 @@ public Index complete() { propagateTypeVariables(); try { - return new Index(masterAnnotations, subclasses, subinterfaces, implementors, classes, modules, users); + Map> userLists = new HashMap<>(); + for (Map.Entry> entry : users.entrySet()) { + userLists.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return new Index(masterAnnotations, subclasses, subinterfaces, implementors, classes, modules, userLists); } finally { masterAnnotations = null; subclasses = null; diff --git a/core/src/test/java/org/jboss/jandex/test/KnownUsersTest.java b/core/src/test/java/org/jboss/jandex/test/KnownUsersTest.java new file mode 100644 index 00000000..ac8762a5 --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/KnownUsersTest.java @@ -0,0 +1,110 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.Index; +import org.jboss.jandex.test.util.IndexingUtil; +import org.junit.jupiter.api.Test; + +public class KnownUsersTest { + static class SuperClass { + } + + interface ImplementedInterface1 { + } + + interface ImplementedInterface2 { + } + + static class TestClass extends SuperClass + implements ImplementedInterface1, ImplementedInterface2 { + int i; + + Map> m; + + TestClass(StringBuilder str) { + m = new HashMap<>(); + m.put("foo", new ArrayList<>()); + } + + , W extends Exception> U bar(Set s, Queue q) + throws IllegalArgumentException, IllegalStateException, W { + // `toString()` to force a class reference to `File` into the constant pool + Paths.get("").toFile().toString(); + return null; + } + + static class NestedClass { + } + + class InnerClass { + } + } + + @Test + public void test() throws IOException { + Index index = Index.of(SuperClass.class, ImplementedInterface1.class, ImplementedInterface2.class, TestClass.class); + doTest(index); + doTest(IndexingUtil.roundtrip(index)); + } + + private void doTest(Index index) { + // from class signature + assertKnownUsers(index, SuperClass.class); + assertKnownUsers(index, ImplementedInterface1.class); + assertKnownUsers(index, ImplementedInterface2.class); + assertKnownUsers(index, Number.class); + assertKnownUsers(index, CharSequence.class); + assertKnownUsers(index, RuntimeException.class); + // from field types + assertKnownUsers(index, String.class); + assertKnownUsers(index, Integer.class); + // from method signatures + assertKnownUsers(index, StringBuilder.class); + assertKnownUsers(index, Collection.class); + assertKnownUsers(index, Exception.class); + assertKnownUsers(index, Set.class); + assertKnownUsers(index, Long.class); + assertKnownUsers(index, Queue.class); + assertKnownUsers(index, Double.class); + assertKnownUsers(index, IllegalArgumentException.class); + assertKnownUsers(index, IllegalStateException.class); + // from method bodies (class references in the constant pool) + assertKnownUsers(index, HashMap.class); + assertKnownUsers(index, ArrayList.class); + assertKnownUsers(index, Paths.class); + assertKnownUsers(index, Path.class); + assertKnownUsers(index, File.class); + // member classes (class references in the constant pool) + assertKnownUsers(index, TestClass.NestedClass.class); + assertKnownUsers(index, TestClass.InnerClass.class); + } + + private void assertKnownUsers(Index index, Class clazz) { + Collection knownUsers = index.getKnownUsers(clazz); + + assertNotNull(knownUsers); + assertFalse(knownUsers.isEmpty()); + for (ClassInfo knownUser : knownUsers) { + if (TestClass.class.getName().equals(knownUser.name().toString())) { + return; + } + } + fail("Expected " + TestClass.class.getName() + " to be a known user of " + clazz.getName()); + } +}