OpenRewrite Learning Notes (4): Creating Complex LST with JavaTemplate
LST (Lossless Semantic Tree) is the core of OpenRewrite and a key pillar for its precise and controllable code modifications. This article introduces how to use JavaTemplate to create complex LST.
Background
When manipulating code, tasks like adding, modifying, or deleting code snippets (such as declaring a variable, adding a method, or changing a method body) often require creating LST nodes. Manually constructing these nodes — like J.VariableDeclarations
and J.VariableDeclarations.NamedVariable
—can be cumbersome, as it involves initializing numerous parameters with accurate type information, as shown in the following diagrams:
Thankfully, OpenRewrite provides JavaTemplate to simplify this process. Instead of manual construction, you can generate valid LST nodes directly from code snippets:
JavaTemplate javaTemplate = JavaTemplate.builder("List<String> list = new ArrayList<>();").build();
This creates a properly formatted LST for a variable declaration and assignment, ensuring correct type information without manual parameter handling.
Constructing JavaTemplate
Parameterized Templates
JavaTemplate is created from a string-based code snippet, which must be valid Java and can reference variables, methods, and symbols visible within the lexical scope of the insertion point. Dynamic parameterization is achieved using placeholders: #{}
(untyped) and #{any(<type>)}
(typed), where the latter enforces type checking for the inserted LST.
Untyped Placeholders
Untyped placeholders (e.g., #{}
) are used for context-agnostic insertions like annotations, class/method names, or keywords, without type checking. For example:
JavaTemplate.builder("public String #{}() { return \"Hello from #{}!\"; }").build();
Here, the first placeholder inserts a method name (as a J.Identifier
node), and the second inserts a string literal.
Typed Placeholders
Typed placeholders (e.g., #{any(<type>)}
) enforce type checking and accept LST nodes as arguments. For instance, to ensure a placeholder expects a String
type:
boolean b = #{any(boolean)};
foo.acceptsACollection(#{any(java.util.Collection)});
String[] = #{anyArray(java.lang.String)};
In a refactoring scenario where arg.toString().toCharArray()
should become arg.toCharArray()
, using #{any(java.lang.String)}
ensures the inserted node (e.g., arg
) has the correct type.
Context Sensitivity
A template is context-sensitive if it references symbols (classes, variables, methods) visible in the insertion scope (e.g., int a = b
where b
is a local variable). Use contextSensitive()
when the template depends on such symbols:
JavaTemplate getter = JavaTemplate
.builder("public #{} #{}() { return #{any()}; }")
.contextSensitive()
.build();
Type Awareness
By default, JavaTemplate uses JavaParser
to resolve types from the Java runtime. For external library types, explicitly configure imports or classpaths:
Adding Imports
Include necessary imports for template types:
JavaTemplate.builder("List<String> list = new ArrayList<>();")
.imports("java.util.List", "java.util.ArrayList")
.build();
Referencing External Types
Use classpath()
to resolve types from the project’s runtime classpath (e.g., Spring annotations):
JavaTemplate.builder("@RequestMapping#{}")
.javaParser(JavaParser.fromJavaVersion().classpath("spring-web"))
.build();
For classes not in the classpath, provide stubs using dependsOn()
:
JavaTemplate.builder("@" + annotationSimpleName + "#{}")
.javaParser(JavaParser.fromJavaVersion()
.dependsOn("package org.springframework.web.bind.annotation;\npublic @interface RequestMapping { }")
)
.build();
Usage
After constructing a JavaTemplate, use apply(...)
to insert it into the LST, specifying Coordinates to define the insertion point.
Coordinates: Defining Insertion Points
Coordinates specify how a template is applied to an LST. For example, a J.ClassDeclaration
provides coordinates for adding annotations, modifying generics, or implementing interfaces. Check available coordinates in org.openrewrite.java.tree.CoordinateBuilder
.
Example: Adding a Method to a Class
The following recipe adds a hello
method to a class, using JavaTemplate and coordinates to ensure correct insertion:
public class AddHelloMethod extends Recipe {
@Override
public String getDisplayName() {
return "Add Hello Method";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
private final JavaTemplate helloTemplate = JavaTemplate.builder(
"public String hello() { return \"Hello from #{}!\"; }"
).build();
private J.ClassDeclaration targetClass; @Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
this.targetClass = classDecl;
return super.visitClassDeclaration(classDecl, ctx);
} @Override
public J.Block visitBlock(J.Block block, ExecutionContext ctx) {
// Insert method into class block if it doesn't exist
if (getCursor().getParent().getValue() == targetClass &&
!getCursor().getMessage("methodExists", false)) {
block = helloTemplate.apply(
getCursor(),
block.getCoordinates().lastStatement(),
targetClass.getType().getClassName()
);
}
return block;
} @Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
// Mark if 'hello' method already exists
if ("hello".equals(method.getSimpleName())) {
getCursor().putMessage("methodExists", true);
}
return method;
}
};
}
}
Key steps:
- Define the template with a placeholder for the class name.
- Use
visitClassDeclaration
to track the target class. - In
visitBlock
, check if the class block is the target and insert the method usinglastStatement()
coordinates. - Mark existing
hello
methods to avoid duplication.
Summary
JavaTemplate simplifies creating complex LST nodes by leveraging code snippets and type-safe placeholders, eliminating manual node construction. By combining it with coordinates and context-sensitive resolution, you can accurately manipulate code while preserving semantic and syntactic details — essential for robust code refactoring recipes in OpenRewrite.