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:
+ *
+ * - occur in the signature of the class (that is, in the superclass type,
+ * in the superinterface types, in the type parameters, or in the list of
+ * permitted subclasses), 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, as described
+ * by the JLS and JVMS.
+ *
*
* @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:
+ *
+ * - occur in the signature of the class (that is, in the superclass type,
+ * in the superinterface types, in the type parameters, or in the list of
+ * permitted subclasses), 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, as described
+ * by the JLS and JVMS.
+ *
*
* @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:
+ *
+ * - occur in the signature of the class (that is, in the superclass type,
+ * in the superinterface types, in the type parameters, or in the list of
+ * permitted subclasses), 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 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 extends Long> s, Queue super Double> 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());
+ }
+}