5.3. Using Cursors#
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:
import assert from "node:assert";
import { ParseOutput, Parser } from "@nomicfoundation/slang/parser";
export function createTree(): ParseOutput {
const source = `
contract Foo {
function foo_func() {}
}
contract Bar {
function bar_func() {}
}
contract Baz {
function baz_func() {}
}
`;
const parser = Parser.create("0.8.28");
const parseOutput = parser.parseFileContents(source.trim());
assert(parseOutput.isValid());
return parseOutput;
}
Listing Contract Names#
The below example uses a cursor to list the names of all contracts in a source file:
import assert from "node:assert";
import { createTree } from "./common.mjs";
import { NonterminalKind, TerminalKind } from "@nomicfoundation/slang/cst";
test("listing contract names", () => {
const tree = createTree();
const cursor = tree.createTreeCursor();
const contracts = [];
while (cursor.goToNextNonterminalWithKind(NonterminalKind.ContractDefinition)) {
assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier));
contracts.push(cursor.node.unparse());
}
assert.deepStrictEqual(contracts, ["Foo", "Bar", "Baz"]);
});
Visiting Only a Sub-tree#
Next, we will try to get the names of all contract functions, grouped by the contract name. In this case, it is not enough to just visit all instances of FunctionDefinition
nodes, since we want to exclude the ones that are not part of a contract.
We need first to find all ContractDefinition
nodes, and then for each contract, look for all FunctionDefinition
nodes, limiting the search to just the contract's subtree. To do that, we need to use the cursor.spawn()
API, which cheaply creates a new cursor that starts at the given node, without copying the ancestry information, so it will only be able to see the sub-tree of the current node.
import assert from "node:assert";
import { createTree } from "./common.mjs";
import { NonterminalKind, TerminalKind } from "@nomicfoundation/slang/cst";
test("visiting subtrees", () => {
const tree = createTree();
const cursor = tree.createTreeCursor();
const results: { [contractName: string]: string[] } = {};
while (cursor.goToNextNonterminalWithKind(NonterminalKind.ContractDefinition)) {
const childCursor = cursor.spawn();
assert(childCursor.goToNextTerminalWithKind(TerminalKind.Identifier));
const contractName = childCursor.node.unparse();
results[contractName] = [];
while (childCursor.goToNextNonterminalWithKind(NonterminalKind.FunctionDefinition)) {
assert(childCursor.goToNextTerminalWithKind(TerminalKind.Identifier));
results[contractName].push(childCursor.node.unparse());
}
}
assert.deepStrictEqual(results, {
Foo: ["foo_func"],
Bar: ["bar_func"],
Baz: ["baz_func"],
});
});
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 line number of each function, along with its text:
import assert from "node:assert";
import { createTree } from "./common.mjs";
import { NonterminalKind } from "@nomicfoundation/slang/cst";
test("accessing node positions", () => {
const tree = createTree();
const cursor = tree.createTreeCursor();
const functions = [];
while (cursor.goToNextNonterminalWithKind(NonterminalKind.FunctionDefinition)) {
const line = cursor.textRange.start.line;
const text = cursor.node.unparse().trim();
functions.push({ line, text });
}
assert.deepStrictEqual(functions, [
{ line: 1, text: "function foo_func() {}" },
{ line: 4, text: "function bar_func() {}" },
{ line: 7, text: "function baz_func() {}" },
]);
});