smallrye / jandex

Java Annotation Indexer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

IllegalStateException "Required class information is missing" when indexing a local class within an inner class

batchmode opened this issue · comments

Recently I ran into a problem where I could not start a Junit based integration test including Hibernate where the Jandex library threw this exception:

Exception in thread "main" java.lang.IllegalStateException: Required class information is missing
	at org.jboss.jandex.Indexer.searchNestedType(Indexer.java:1255)
	at org.jboss.jandex.Indexer.searchTypePath(Indexer.java:1169)
	at org.jboss.jandex.Indexer.updateTypeTarget(Indexer.java:1139)
	at org.jboss.jandex.Indexer.updateTypeTargets(Indexer.java:849)
	at org.jboss.jandex.Indexer.index(Indexer.java:1980)

After some debugging and testing I assume that this seems to be related with local classes within an inner class having an annotated field with the type of another local class. The annotation has to have the TYPE_USE target.

I can reproduce the problem with following code (using jandex 2.4.1.Final):

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.jboss.jandex.Indexer;


public class ReproduceMissingClassInfo {

    public static void main(String[] args) throws IOException {

        dumpSystemProperties();

        //reproducing code
        InputStream input = ReproduceMissingClassInfo.class.getResourceAsStream("/ReproduceMissingClassInfo$InnerClass$1LocalClass.class");

        new Indexer().index(input);
    }

    class InnerClass {

        void doSomeThing() {

            class LocalNestedClass {
            }

            class LocalClass {

                @SomeAnnotation
                private LocalNestedClass nested;

            }
        }
    }

    @Target({FIELD, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    @interface SomeAnnotation {

    }

    private static void dumpSystemProperties() {

        System.out.println("SYSTEM PROPERTIES:\n");
        System.getProperties().entrySet().stream()
                .map(entry -> String.format("%s = %s", entry.getKey(), entry.getValue()).replaceAll(System.getProperty("user.name"), "xxxx").replaceAll("xxxx/.*target", "xxxx/.../target"))
                .forEach(System.out::println);
        System.out.println();
    }
}

On my environment running this produces following output:

SYSTEM PROPERTIES:

sun.desktop = windows
awt.toolkit = sun.awt.windows.WToolkit
java.specification.version = 11
sun.cpu.isalist = amd64
sun.jnu.encoding = Cp1252
java.class.path = E:\devel\workspace-idea\reproduce-jandex-exception\target\classes;C:\Users\xxxx\.m2\repository\org\jboss\jandex\2.4.1.Final\jandex-2.4.1.Final.jar
java.vm.vendor = AdoptOpenJDK
sun.arch.data.model = 64
user.variant = 
java.vendor.url = https://adoptopenjdk.net/
user.timezone = 
os.name = Windows 10
java.vm.specification.version = 11
sun.java.launcher = SUN_STANDARD
user.country = DE
sun.boot.library.path = C:\Program Files\AdoptOpenJDK\jdk-11.0.3.7-hotspot\bin
sun.java.command = ReproduceMissingClassInfo
jdk.debug = release
sun.cpu.endian = little
user.home = C:\Users\xxxx
user.language = de
java.specification.vendor = Oracle Corporation
java.version.date = 2019-04-16
java.home = C:\Program Files\AdoptOpenJDK\jdk-11.0.3.7-hotspot
file.separator = \
java.vm.compressedOopsMode = Zero based
line.separator = 

java.specification.name = Java Platform API Specification
java.vm.specification.vendor = Oracle Corporation
java.awt.graphicsenv = sun.awt.Win32GraphicsEnvironment
user.script = 
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
java.runtime.version = 11.0.3+7
user.name = xxxx
path.separator = ;
os.version = 10.0
java.runtime.name = OpenJDK Runtime Environment
file.encoding = UTF-8
java.vm.name = OpenJDK 64-Bit Server VM
java.vendor.version = AdoptOpenJDK
java.vendor.url.bug = https://github.com/AdoptOpenJDK/openjdk-build/issues
java.io.tmpdir = C:\Users\MARCE_~1\AppData\Local\Temp\
java.version = 11.0.3
user.dir = E:\devel\workspace-idea\reproduce-jandex-exception
os.arch = amd64
java.vm.specification.name = Java Virtual Machine Specification
java.awt.printerjob = sun.awt.windows.WPrinterJob
sun.os.patch.level = 
java.library.path = C:\Program Files\AdoptOpenJDK\jdk-11.0.3.7-hotspot\bin;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\Program Files\AdoptOpenJDK\jdk-11.0.3.7-hotspot\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\ATI Technologies\ATI.ACE\Core-Static;C:\Program Files (x86)\AMD\ATI Technologies\ATI.ACE\Core-Static;C:\Program Files\OpenVPN\bin;C:\Program Files (x86)\GtkSharp\2.12\bin;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;C:\Program Files\Git\cmd;C:\Program Files\Go\bin;C:\Users\xxxx\scoop\apps\nodejs\current\bin;C:\Users\xxxx\scoop\apps\nodejs\current;C:\Users\xxxx\scoop\shims;C:\Program Files (x86)\Git\bin;E:\Tools\gource-0.39.win32;E:\Tools\apache-maven-3.0.4\bin;C:\Users\xxxx\AppData\Local\Microsoft\WindowsApps;E:\Tools\micronaut\micronaut-1.2.1\bin;E:\Tools\gradle\gradle-5.6.2\bin;C:\Users\xxxx\AppData\Local\atom\bin;C:\ProgramData\xxxx\atom\bin;C:\Users\xxxx\AppData\Local\Programs\Microsoft VS Code\bin;C:\Program Files (x86)\FAHClient;C:\Users\xxxx\AppData\Local\Microsoft\WindowsApps;C:\Users\xxxx\go\bin;.
java.vendor = AdoptOpenJDK
java.vm.info = mixed mode
java.vm.version = 11.0.3+7
sun.io.unicode.encoding = UnicodeLittle
java.class.version = 55.0

Exception in thread "main" java.lang.IllegalStateException: Required class information is missing
	at org.jboss.jandex.Indexer.searchNestedType(Indexer.java:1255)
	at org.jboss.jandex.Indexer.searchTypePath(Indexer.java:1169)
	at org.jboss.jandex.Indexer.updateTypeTarget(Indexer.java:1139)
	at org.jboss.jandex.Indexer.updateTypeTargets(Indexer.java:849)
	at org.jboss.jandex.Indexer.index(Indexer.java:1980)
	at ReproduceMissingClassInfo.main(ReproduceMissingClassInfo.java:23)

Process finished with exit code 1

The same issue also occured with OpenJDK 17 (compile and runtime).

Looking at the relevant code in Jandex, I found #92 / #111 and I think this might be a similar case.

I changed the reproducer to this:

public class Reproducer {
    @Target(ElementType.TYPE_USE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface SomeAnnotation {
    }

    class InnerClass {
        class InnerInnerClass {}

        Object doSomething() {
            class LocalClass {}

            class AnotherLocalClass {
                @SomeAnnotation
                LocalClass local;

                @SomeAnnotation
                InnerInnerClass inner;
            }

            return new AnotherLocalClass();
        }
    }

    public static void main(String[] args) throws IOException {
        Index.of(new Reproducer().new InnerClass().doSomething().getClass());
    }
}

When I compile this class using javac from OpenJDK 17.0.1 and then decompile the Reproducer\$InnerClass\$1AnotherLocalClass.class file using javap -v, I get the following output for the 2 fields:

  org.jboss.jandex.test.Reproducer$InnerClass$1LocalClass local;
    descriptor: Lorg/jboss/jandex/test/Reproducer$InnerClass$1LocalClass;
    flags: (0x0000)
    RuntimeVisibleTypeAnnotations:
      0: #16(): FIELD, location=[INNER_TYPE, INNER_TYPE]
        org.jboss.jandex.test.Reproducer$SomeAnnotation

  org.jboss.jandex.test.Reproducer$InnerClass$InnerInnerClass inner;
    descriptor: Lorg/jboss/jandex/test/Reproducer$InnerClass$InnerInnerClass;
    flags: (0x0000)
    RuntimeVisibleTypeAnnotations:
      0: #16(): FIELD, location=[INNER_TYPE, INNER_TYPE]
        org.jboss.jandex.test.Reproducer$SomeAnnotation

This kinda sorta makes sense for the inner field, whose type is actually Reproducer.InnerClass.InnerInnerClass (and each of those type usages may be annotated separately). I think it doesn't make sense for the local field, because you can't really write its type as Reproducer.InnerClass.EmptyLocalClass or anything like that. So I'd guess that the type annotation's type path should be empty.

The JVM specification, chapter 4.7.6. The InnerClasses Attribute, says in the description of outer_class_info_index:

If C is not a member of a class or an interface - that is, if C is a top-level class or interface (JLS §7.6) or a local class (JLS §14.3) or an anonymous class (JLS §15.9.5) - then the value of the outer_class_info_index item must be zero.

The JLS, chapter 14.3. Local Class and Interface Declarations, says:

Like an anonymous class (§15.9.5), a local class or interface is not a member of any package, class, or interface (§7.1, §8.5).

Further, the JLS, chapter 8. Classes, says:

A nested class is any class whose declaration occurs within the body of another class or interface declaration. A nested class may be a member class (§8.5, §9.5), a local class (§14.3), or an anonymous class (§15.9.5).

This all leads me to believe that my assessment above is correct: LocalClass is not a member class and type of the local field can't be written like as Reproducer.InnerClass.EmptyLocalClass or anything similar.

Need to dig more into the specifications though, these are just preliminary results.

The JVM specification, chapter 4.7.6. The InnerClasses Attribute, says in the description of outer_class_info_index:

If C is not a member of a class or an interface - that is, if C is a top-level class or interface (JLS §7.6) or a local class (JLS §14.3) or an anonymous class (JLS §15.9.5) - then the value of the outer_class_info_index item must be zero.

Keep in mind that in the anon or local case you also have the EnclosingMethod attribute, which we do analyze. I haven't looked at the code, but it could just be we aren't tracking non-inner types for this search. I might have either misinterpreted this being allowed, or it could just be an oversight. One thing that is confusing is the names that javap prints aren't necessarily reflective of the true definition (e.g. It's not really INNER but "Nested" in terms of the spec). I have also ecountered invalid data in this tabl (e.g. type annotations with bridge methods) e, so its good to double check the correctness aspect and potentially ignore.

Just as an experiment, I tried to compile the class with ECJ 4.22 and it emits the same type path. I'll continue digging.

Ah and using the workaround from #111 for this situation seems to produce expected results. So that's a possible way to go.

I spent some time digging around (mostly in JLS and JVMS, but I also looked at the original JSR 308 specification) and I think my original assessment is wrong. Assuming both specifications are absolutely precise, that is.

JLS, chapter 8. Classes, says:

A nested class may be a member class (§8.5, §9.5), a local class (§14.3), or an anonymous class (§15.9.5).

That is, a local class is still a nested class (even though it is not a member class).

(Aside: funnily enough, per chapter 8.1.3. Inner Classes and Enclosing Instances, local classes are also inner classes. That doesn't matter here, because the JVMS virtually never refers to inner types outside of the specification of the InnerClasses attribute.)

JVMS, chapter 4.7.20.2. The type_path structure, says:

If a nested type T1.T2 is used in a declaration or expression, then an annotation may appear on the name of the innermost member type and any enclosing type for which a type annotation is admissible (JLS §9.7.4).

This shows that the JVMS makes a difference between nested types and member types which is consistent with how the JLS differentiates between these terms.

(Aside: at the same time, it also leaves the case of non-member nested types a little undefined...)

Further, it says:

The value of the path_length item gives the number of entries in the path array:

  • If the value of path_length is 0, and the type being annotated is a nested type, then the annotation applies to the outermost part of the type for which a type annotation is admissible.
  • If the value of path_length is 0, and the type being annotated is not a nested type, then the annotation appears directly on the type itself.
  • If the value of path_length is non-zero, then each entry in the path array represents an iterative, left-to-right step towards the precise location of the annotation in an array type, nested type, or parameterized type.

[...]

Value Interpretation
... ...
1 Annotation is deeper in a nested type
... ...

That is, all descriptions of the type path in JVMS use the term "nested type". While the enclosing classes of a local class can't be denoted in the local class name, and hence can't be annotated, they are still technically there.

I'll see what Jandex can do about it.

I'm looking into this again and I found that there's one more bug: Jandex doesn't recognize a type annotation on the outermost type in a nested type path. For example, this piece of code

public class Reproducer {
    @Target(ElementType.TYPE_USE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TypeAnn {
        String value();
    }

    class Inner {
        class InnerInner {
            @TypeAnn("reproducer") Reproducer.@TypeAnn("inner") Inner.@TypeAnn("inner inner") InnerInner field;
        }
    }

    public static void main(String[] args) throws IOException {
        Index index = Index.of(Inner.InnerInner.class);
        ClassInfo clazz = index.getKnownClasses().iterator().next();
        System.out.println(clazz.field("field").type());
    }
}

prints

@TypeAnn(value = "inner") org.jboss.jandex.Reproducer$Inner.@TypeAnn(value = "inner inner") @TypeAnn(value = "reproducer") InnerInner

The @TypeAnn("reproducer") annotation is associated with the InnerInner type, instead of the Reproducer type.

There's a comprehensive test for type annotations that includes nested type paths like this, but interestingly, it doesn't cover the case of an annotated outermost type.

Done in #205.