From 1d0538995a3257ed4152aaed5f6606fadb47f4fd Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 4 Mar 2024 12:53:02 +0100 Subject: [PATCH 1/2] improve ClassInfo.recordComponents() in case the class is not a record --- core/src/main/java/org/jboss/jandex/ClassInfo.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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() { From 3a80388a041df4a5995e50b61766267159140bb2 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 4 Mar 2024 13:30:56 +0100 Subject: [PATCH 2/2] 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, permitted subclasses), 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 | 42 ++++++- .../main/java/org/jboss/jandex/Indexer.java | 90 ++++++++++++-- .../org/jboss/jandex/test/KnownUsersTest.java | 110 ++++++++++++++++++ 3 files changed, 227 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 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: + *
    + *
  • 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 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()); + } +}