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: + * * * @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()); + } +}