-
Notifications
You must be signed in to change notification settings - Fork 30
Visitors and Cursors
AST visitors, extending AstVisitor<T>
, allow for event-based inspection of code, yielding a value T
at the end of the tree traversal. All of rewrite's search and refactoring operations are implemented as AstVisitor
's and several other helper visitors pack with the library. Cursors are a representation of the path of a particular AST node relative to its containing nodes.
Below is an example class that we will use to demonstrate a visitor that collects String literals.
public class A {
String s1 = "1";
String s2 = "2";
}
class B {
String sArr = new String[] { "3", "4" };
}
Here is the visitor:
class AllStrings extends AstVisitor<List<String>> {
public AllStrings() { super(Collections.emptyList()); }
@Override
public List<String> visitLiteral(Tr.Literal literal) {
if(literal.getTypeTag() == TypeTag.String)
return Collections.singletonList((String) literal.getValue());
return super.visitLiteral(literal);
}
}
To collect all Strings in the entire compilation unit (the whole source file), we can call visit on the Tr.CompilationUnit
:
Tr.CompilationUnit cu = parser.parse(cu);
new AllStrings().visit(cu); // returns list ["1", "2", "3", "4"]
Alternatively, we can visit a more targeted portion of the AST, such as a particular class:
new AllStrings().visit(cu.getFirstClass()); // returns list ["1", "2"]
Visitors contain a reduce(T t1, T t2)
method that governs how returns from the visits of various AST nodes are aggregated. The AstVisitor<T>
base class provides some sensible defaults:
- If
T
is anIterable
,reduce
performs list concatenation - If
T
isBoolean
,reduce
returnst1 || t2
. Existence-style search visitors generally returnBoolean
. - Otherwise,
reduce
returnst1
if not null andt2
otherwise.
You are free to override reduce
in your visitor to provide a custom aggregation strategy.
Cursors maintain the stack of AST nodes that led to a particular invocation of one of the visit
methods inside an AstVisitor
. Inside any visit
method, you can access the current cursor by calling cursor()
. Note that the cursor that cursor()
returns is relative to the first node visited.
Consider this visitor:
class CallEmptyListCursor extends AstVisitor<Cursor> {
@Override
public Cursor visitMethodInvocation(@NotNull Tr.MethodInvocation meth) {
if(meth.getSimpleName().equals("emptyList"))
return cursor();
return super.visitMethodInvocation(meth);
}
}
Here is our sample source file:
import static java.util.Collections.*;
public class A {
public void foo() {
List l = emptyList();
}
}
First, we will visit this AST starting at the compilation unit level, and print out the types of the AST elements in the path:
Tr.CompilationUnit a = parser.parse(a);
Cursor c = new CallEmptyListCursor().visit(a);
The path elements of cursor c
are as follows in order:
Type | Description |
---|---|
Tr.CompilationUnit | The entire source file |
Tr.ClassDecl | Class A
|
Tr.Block | The body of class A
|
Tr.MethodDecl | The method declaration foo
|
Tr.Block | The body of method declaration foo
|
Tr.VariableDecls | The variable declaration statement List l = ...
|
Tr.NamedVar | The named variable l and it's assignment |
Tr.MethodInvocation | The assignment of l , which is a call to emptyList
|
Were we to perform the same visit, but beginning at the method invocation, the cursor path would start at the Tr.MethodDecl
rather than Tr.CompilationUnit
:
Cursor c = new CallEmptyListCursor().visit(a.getFirstClass().methods().get(0));
Given an instance of an AST node, it is always possible to derive its cursor relative to the whole compilation unit by calling cursor(Tree)
on the Tr.CompilationUnit
. This returns the same cursor we have tabled above:
Tr.CompilationUnit a = ...
Tr.MethodInvocation emptyList = ...
a.cursor(emptyList)
This is useful when you retrieve AST elements from some higher-level code search concept (finding annotations, method invocations, fields, etc.) and want to contextualize your search results. The following filters for method invocations of Collections.emptyList
that are enclosed inside the foo
method declaration:
Tr.CompilationUnit a = ...
a.findMethodCalls("java.util.Collections emptyList()").stream()
.filter(m -> a.cursor(m).enclosingMethod().getSimpleName().equals("foo"));