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 {}
import assert from "node:assert";
import { Parser } from "@nomicfoundation/slang/parser";
import { assertIsTerminalNode, NonterminalKind, TerminalKind } from "@nomicfoundation/slang/cst";

const parser = Parser.create("0.8.0");

const parseOutput = 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:

const contracts = [];

const cursor = parseOutput.createTreeCursor();

while (cursor.goToNextNonterminalWithKind(NonterminalKind.ContractDefinition)) {
  assert(cursor.goToFirstChild());
  assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier));

  assertIsTerminalNode(cursor.node);
  contracts.push(cursor.node.unparse());

  assert(cursor.goToParent());
}

assert.deepStrictEqual(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:

const contracts = [];

const cursor = parseOutput.createTreeCursor();

while (cursor.goToNextNonterminalWithKind(NonterminalKind.ContractDefinition)) {
  const childCursor = cursor.spawn();
  assert(childCursor.goToNextTerminalWithKind(TerminalKind.Identifier));

  assertIsTerminalNode(childCursor.node);
  contracts.push(childCursor.node.unparse());
}

assert.deepStrictEqual(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:

const contracts = [];

const cursor = parseOutput.createTreeCursor();

while (cursor.goToNextNonterminalWithKind(NonterminalKind.ContractDefinition)) {
  const range = cursor.textRange;

  const contractNode = cursor.node;

  contracts.push([
    range.start.line,
    range.start.column,
    range.end.line,
    range.end.column,
    contractNode.unparse().trim(),
  ]);
}

assert.deepStrictEqual(contracts, [
  [0, 0, 1, 0, "contract Foo {}"],
  [1, 0, 2, 0, "contract Bar {}"],
  [2, 0, 2, 15, "contract Baz {}"],
]);