Debugging OpenRewrite AST that contains missing or invalid type information
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
Source Code Example Used in Article:
Support our work
Subscribe to my newsletter
Read articles from BitFlipper directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by