Debugging OpenRewrite AST that contains missing or invalid type information

BitFlipperBitFlipper
4 min read

You’ve finally managed to get your Java refactoring recipe working where it is producing the correct text output. Your unit test is still failing though with:

java.lang.IllegalStateException: AST contains missing or invalid type information

Why are the tests failing?

The tests are failing as your recipe is updating the AST so the text it output is correct however the AST is missing type information.

The missing type information isn’t necessarily a problem when running your recipe in isolation. There will likely be a problem when chaining multiple recipes together. Missing type information will likely cause other recipes to misbehave. The chained recipes won’t be able to reliably get full type information of nodes they want to modify.

How do I fix the missing type information?

We have a simple recipe JavaGetterToLombokGetter that replaces standard Java getters with lombok @Getter annotations. This buggy recipe will be used to demonstrate how to debug recipes that incorrectly modify ASTs with missing type information. The recipe should transform source code like so:

Before

public class TestClass {
    public String foo = "foo";
    public int a = 10;

    public String getFoo() {
        return foo;
    }

    public int getA() {
        return a;
    }
}

After

import lombok.Getter;

public class TestClass {
    @Getter
    public String foo = "foo";
    @Getter
    public int a = 10;
}
See Full Recipe Source Code and Unit Tests

1. Identify where type information is missing

java.lang.IllegalStateException: LST contains missing or invalid type information
Identifier->Annotation->VariableDeclarations->Block->ClassDeclaration->CompilationUnit
/*~~(Identifier type is missing or malformed)~~>*/Getter

Identifier->Annotation->VariableDeclarations->Block->ClassDeclaration->CompilationUnit
/*~~(Identifier type is missing or malformed)~~>*/Getter

Above is the relevant sample of the error message from a failing test. The error message contains some clues as to what is wrong. To more easily identify the missing type information we can use a helpful debugging OpenRewrite recipe; Find missing type information on Java ASTs.

The easiest way to use this recipe is to invoke it after running your recipe.

@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) {
    // Add the following at the top of one of your visit methods
    doAfterVisit(new FindMissingTypes().getVisitor());
    ...
}

The debugging recipe will go over the AST and put comments in the source code output where type information is missing or invalid. In our getter example the new test output is the following:

import lombok.Getter;

public class TestClass {
    @/*~~(Identifier type is missing or malformed)~~>*/Getter
    public String foo = "foo";
    @/*~~(Identifier type is missing or malformed)~~>*/Getter
    public int a = 10;
}

We can see /*~~(Identifier type is missing or malformed)~~>*/ comments have been added to the @Getter annotations. This clearly shows us the type information associated with the annotations is wrong. These annotations were added by our recipe so we need to modify the recipe so the correct type information is given.

2. Fix your recipe so it correctly updates all missing types

Our recipe uses a JavaTemplate to create the new @Getter annotation nodes.

private J.VariableDeclarations addLombokGetterAnnotationTo(J.VariableDeclarations multiVariable) {
    return JavaTemplate.builder("@Getter")
                .javaParser(JavaParser.fromJavaVersion())
                .build()
        .apply(getCursor(), multiVariable.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::toString)));
}

The above template is incorrect as it produces an output that does not include type information. To fix this we need to tell the template where to look to find the correct type information. The most common way to do this will be to get the template to look at the runtime classpath. This can be done with .classpath("<relevant-artifactId-here"). We also need to specify the relevant imports with either .imports("<relevant-fully-qualified-import>") or .staticImports("<relevant-fully-qualified-import>").

The example above corrected to output correct type information:

private J.VariableDeclarations addLombokGetterAnnotationTo(J.VariableDeclarations multiVariable) {
    return JavaTemplate.builder("@Getter")
                .javaParser(
                        JavaParser.fromJavaVersion()
                            .classpath("lombok"))
                .imports(Getter.class.getTypeName())
                .build()
        .apply(getCursor(), multiVariable.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::toString)));
}

Not using a JavaTemplate?

You should be using a JavaTemplate where possible as this is less error prone than the commonly used alternative of manually constructing AST nodes by hand. However if you are manually constructing the nodes you might have some code like this instead:

private J.VariableDeclarations addLombokGetterAnnotationTo(J.VariableDeclarations multiVariable) {
    List<J.Annotation> annotations = new ArrayList<>(multiVariable.getLeadingAnnotations());

    J.Identifier getter = new J.Identifier(UUID.randomUUID(), Space.EMPTY, Markers.EMPTY, "Getter\n    ", null, null);
    annotations.add(new J.Annotation(UUID.randomUUID(), Space.EMPTY, new Markers(UUID.randomUUID(), List.of()), getter, null));

    return multiVariable.withLeadingAnnotations(annotations);
}

Like the original JavaTemplate above this hand crafted J.Identifier is missing the appropriate type information. To fix this we need to pass valid type information to the J.Identifier‘s constructor instead of null. We can build the type info with this static factory method JavaType.*buildType*("<relevant-fully-qualified-import>"). The fixed code looks like:

private J.VariableDeclarations addLombokGetterAnnotationTo(J.VariableDeclarations multiVariable) {
    List<J.Annotation> annotations = new ArrayList<>(multiVariable.getLeadingAnnotations());

    J.Identifier getter = new J.Identifier(UUID.randomUUID(), Space.EMPTY, Markers.EMPTY, "Getter\n    ", JavaType.buildType("lombok.Getter"), null);
    annotations.add(new J.Annotation(UUID.randomUUID(), Space.EMPTY, new Markers(UUID.randomUUID(), List.of()), getter, null));

    return multiVariable.withLeadingAnnotations(annotations);
}

References

Support our work

0
Subscribe to my newsletter

Read articles from BitFlipper directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

BitFlipper
BitFlipper