Skip to content

Visitors and Cursors

Jon Schneider edited this page Nov 23, 2016 · 2 revisions

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.

An example visitor

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:

  1. If T is an Iterable, reduce performs list concatenation
  2. If T is Boolean, reduce returns t1 || t2. Existence-style search visitors generally return Boolean.
  3. Otherwise, reduce returns t1 if not null and t2 otherwise.

You are free to override reduce in your visitor to provide a custom aggregation strategy.

Cursors

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"));