diff --git a/core/src/main/java/org/jboss/jandex/ClassInfo.java b/core/src/main/java/org/jboss/jandex/ClassInfo.java index 80e0ab71..dc591a40 100644 --- a/core/src/main/java/org/jboss/jandex/ClassInfo.java +++ b/core/src/main/java/org/jboss/jandex/ClassInfo.java @@ -885,12 +885,14 @@ public final RecordComponentInfo recordComponent(String name) { /** * Returns a list of all record components declared by this class. - * This list may be empty, but never null. + * This list may be empty, but never {@code null}. + *

+ * If this class is not a record, returns an empty list. * - * @return a list of record components + * @return immutable list of record components */ public final List recordComponents() { - if (extra == null || extra.recordComponents == null) { + if (!isRecord() || extra == null || extra.recordComponents == null) { return Collections.emptyList(); } @@ -907,7 +909,7 @@ public final List recordComponents() { * assumes that the bytecode order corresponds to declaration order, which is not guaranteed, * but practically always holds. * - * @return a list of record components + * @return immutable list of record components * @since 2.4 */ public final List unsortedRecordComponents() { diff --git a/core/src/main/java/org/jboss/jandex/IndexView.java b/core/src/main/java/org/jboss/jandex/IndexView.java index 3144cc6e..ab5fefdb 100644 --- a/core/src/main/java/org/jboss/jandex/IndexView.java +++ b/core/src/main/java/org/jboss/jandex/IndexView.java @@ -467,8 +467,18 @@ 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 @@ -476,8 +486,18 @@ 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 @@ -487,8 +507,18 @@ 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 62f7b3ce..6c403dfb 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; @@ -417,7 +418,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(); @@ -442,7 +443,7 @@ private void initIndexMaps() { modules = new HashMap(); if (users == null) - users = new HashMap>(); + users = new HashMap>(); if (names == null) names = new NameTable(); @@ -1106,6 +1107,7 @@ private void resolveTypeAnnotations() { } private void resolveUsers() throws IOException { + // class references in constant pool int poolSize = constantPoolSize; byte[] pool = constantPool; int[] offsets = constantPoolOffsets; @@ -1115,14 +1117,80 @@ 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); + } + for (DotName permittedSubclass : currentClass.permittedSubclasses()) { + recordUsedClass(permittedSubclass); + } + // 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() { @@ -2583,7 +2651,11 @@ public Index complete() { propagateTypeVariables(); try { - return Index.create(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 Index.create(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()); + } +}