Skip to content

Using the Cursor#

This guide will walk you through the basics of using a CST cursor in your project. Let's start with this source file, that contains three contracts:

input.sol
contract Foo {}
contract Bar {}
contract Baz {}
use semver::Version;
use slang_solidity::cst::{EdgeLabel, NonterminalKind, TerminalKind, TextRangeExtensions};
use slang_solidity::parser::Parser;

let parser = Parser::create(Version::parse("0.8.0")?)?;

let parse_output = parser.parse(NonterminalKind::SourceUnit, source);

Listing Contract Names#

The below example uses a cursor to list the names of all contracts in a source file:

let mut contracts = Vec::new();

let mut cursor = parse_output.create_tree_cursor();

while cursor.go_to_next_nonterminal_with_kind(NonterminalKind::ContractDefinition) {
    assert!(cursor.go_to_first_child());
    assert!(cursor.go_to_next_terminal_with_kind(TerminalKind::Identifier));

    let terminal_node = cursor.node();
    contracts.push(terminal_node.as_terminal().unwrap().text.clone());

    // You have to make sure you return the cursor to its original position:
    assert!(cursor.go_to_parent());
}

assert_eq!(contracts, &["Foo", "Bar", "Baz"]);

Visiting Only a Sub-tree#

In cases like the above, we needed to visit a sub-tree of the CST (to get the contract name). But we also need to remember to return the cursor to its original position after each read, which is inconvenient, and can lead to subtle bugs.

To avoid this, we can use the spawn() API, which cheaply creates a new cursor that starts at the given node, without copying the previous path history. This lets us visit the sub-tree of each contract, without modifying the original cursor:

let mut contracts = Vec::new();

let mut cursor = parse_output.create_tree_cursor();

while cursor.go_to_next_nonterminal_with_kind(NonterminalKind::ContractDefinition) {
    let mut child_cursor = cursor.spawn();
    assert!(child_cursor.go_to_next_terminal_with_kind(TerminalKind::Identifier));

    let terminal_node = child_cursor.node();
    contracts.push(terminal_node.as_terminal().unwrap().text.clone());
}

assert_eq!(contracts, &["Foo", "Bar", "Baz"]);

Accessing Node Positions#

The Cursor API also tracks the position and range of the current node it is visiting. Here is an example that records the source range of each contract, along with its text:

let mut contracts = Vec::new();

let mut cursor = parse_output.create_tree_cursor();

while cursor.go_to_next_nonterminal_with_kind(NonterminalKind::ContractDefinition) {
    let range = cursor.text_range().utf8();
    let text = cursor.node().unparse();

    contracts.push((range, text.trim().to_owned()));
}

assert_eq!(
    contracts,
    &[
        (0..16, "contract Foo {}".to_string()),
        (16..32, "contract Bar {}".to_string()),
        (32..47, "contract Baz {}".to_string()),
    ]
);

Using Iterator API#

In addition to the procedural-style methods, the Cursor struct also implements the Iterator trait, which allows you to use it in a functional style.

Let's use that to extract all Identifier nodes from the source text using that API:

let cursor = parse_output.create_tree_cursor();

let identifiers: Vec<_> = cursor
    .filter_map(|node| {
        node.as_terminal_with_kind(TerminalKind::Identifier)
            .cloned()
    })
    .map(|identifier| identifier.text.clone())
    .collect();

assert_eq!(identifiers, &["Foo", "Bar", "Baz"]);

Note

It's important to note that Iterator::next first visits the current node, yields it, and then moves the cursor to the next node. As such, accessor associated functions called on the Cursor that reference the "current" will point to the one that is not yet yielded by the iterator. This might be an important, when mixing the two styles.

Using a Cursor with Names#

In addition to the basic Cursor, there's also a CursorWithLabels type that keeps track of the names of the nodes it visits. You can create a CursorWithLabels from a Cursor by using the with_labels() API.

Let's use that to extract all nodes that are labeled Name:

let cursor = parse_output.create_tree_cursor();

let identifiers: Vec<_> = cursor
    .with_edges()
    .filter(|node| node.label == Some(EdgeLabel::Name))
    .filter_map(|node| {
        node.as_terminal_with_kind(TerminalKind::Identifier)
            .cloned()
    })
    .map(|identifier| identifier.text.clone())
    .collect();

assert_eq!(identifiers, &["Foo", "Bar", "Baz"]);