-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a "How to use the CST cursor" How-To (#729)
Closes #334
- Loading branch information
Showing
9 changed files
with
366 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
4 changes: 4 additions & 0 deletions
4
crates/solidity/outputs/cargo/tests/src/doc_examples/cursor_api/node_accessors.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
contract Foo { | ||
event Transfer(address indexed src, address indexed dst, uint256 value); | ||
event Approval(address indexed owner, address indexed spender, uint256 value); | ||
} |
138 changes: 138 additions & 0 deletions
138
crates/solidity/outputs/npm/tests/src/doc-examples/cursor-api.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// --8<-- [start:step-1-imports] | ||
import { Language } from "@nomicfoundation/slang/language"; | ||
import { FieldName, RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; | ||
import { NodeType } from "@nomicfoundation/slang/cst"; | ||
|
||
const source = ` | ||
contract Foo {} | ||
contract Bar {} | ||
contract Baz {} | ||
`; | ||
|
||
test("construct the cursor", () => { | ||
// --8<-- [start:create-cursor] | ||
const language = new Language("0.8.0"); | ||
const parseTree = language.parse(RuleKind.SourceUnit, source); | ||
|
||
const cursor = parseTree.createTreeCursor(); | ||
// --8<-- [end:create-cursor] | ||
cursor; // Suppress unused warning | ||
}); | ||
|
||
test("list contract names", () => { | ||
const language = new Language("0.8.0"); | ||
const parseTree = language.parse(RuleKind.SourceUnit, source); | ||
|
||
const cursor = parseTree.createTreeCursor(); | ||
// --8<-- [start:example-list-contract-names] | ||
const contractNames: string[] = []; | ||
|
||
while (cursor.goToNextRuleWithKinds([RuleKind.ContractDefinition])) { | ||
// You have to make sure you return the cursor to original position | ||
cursor.goToFirstChild(); | ||
cursor.goToNextTokenWithKinds([TokenKind.Identifier]); | ||
|
||
const tokenNode = cursor.node(); | ||
if (tokenNode.type == NodeType.Token) { | ||
contractNames.push(tokenNode.text); | ||
} else { | ||
throw new Error("Identifier should be a token node"); | ||
} | ||
|
||
cursor.goToParent(); | ||
} | ||
|
||
expect(contractNames).toEqual(["Foo", "Bar", "Baz"]); | ||
}); | ||
|
||
test("use cursor spawn", () => { | ||
const language = new Language("0.8.0"); | ||
const parseTree = language.parse(RuleKind.SourceUnit, source); | ||
|
||
const cursor = parseTree.createTreeCursor(); | ||
// --8<-- [start:example-using-spawn] | ||
const contractNames: string[] = []; | ||
|
||
while (cursor.goToNextRuleWithKinds([RuleKind.ContractDefinition])) { | ||
// `.spawn()` creates a new cursor without the path history, which is cheaper | ||
// than `.clone()`, which copies the path history. | ||
// Do this when you don't want to worry about restoring the position of the | ||
// existing cursor. | ||
const child_cursor = cursor.spawn(); | ||
// You have to make sure you return the cursor to original position | ||
child_cursor.goToNextTokenWithKinds([TokenKind.Identifier]); | ||
|
||
const tokenNode = child_cursor.node(); | ||
if (tokenNode.type == NodeType.Token) { | ||
contractNames.push(tokenNode.text); | ||
} else { | ||
throw new Error("Identifier should be a token node"); | ||
} | ||
} | ||
|
||
expect(contractNames).toEqual(["Foo", "Bar", "Baz"]); | ||
// --8<-- [end:example-using-spawn] | ||
}); | ||
|
||
test("access the node pointed to the cursor", () => { | ||
const source = `contract Foo { | ||
event Transfer(address indexed src, address indexed dst, uint256 value); | ||
event Approval(address indexed owner, address indexed spender, uint256 value); | ||
}`; | ||
|
||
const language = new Language("0.8.0"); | ||
const parseTree = language.parse(RuleKind.SourceUnit, source); | ||
|
||
const cursor = parseTree.createTreeCursor(); | ||
// --8<-- [start:example-node-accessors] | ||
|
||
cursor.goToNextRuleWithKind(RuleKind.EventParameters); | ||
|
||
let parameterRanges: Array<{ textValue: string; range: [number, number] }> = []; | ||
// Only visit children of the first event parameters node | ||
let cursorChild = cursor.spawn(); | ||
while (cursorChild.goToNextRuleWithKind(RuleKind.EventParameter)) { | ||
// Collect the text value for each parameter rule node | ||
let textValue = ""; | ||
const innerCursor = cursorChild.spawn(); | ||
while (innerCursor.goToNextToken()) { | ||
const node = innerCursor.node(); | ||
if (node.type != NodeType.Token) continue; | ||
textValue += node.text; | ||
} | ||
|
||
let range = cursorChild.textRange; | ||
parameterRanges.push({ textValue, range: [range.start.utf8, range.end.utf8] }); | ||
} | ||
|
||
expect(parameterRanges).toEqual([ | ||
{ textValue: "address indexed src", range: [31, 50] }, | ||
{ textValue: " address indexed dst", range: [51, 71] }, | ||
{ textValue: " uint256 value", range: [72, 86] }, | ||
]); | ||
// --8<-- [end:example-node-accessors] | ||
}); | ||
|
||
test("access the node using its name", () => { | ||
const language = new Language("0.8.0"); | ||
const parseTree = language.parse(RuleKind.SourceUnit, source); | ||
|
||
const cursor = parseTree.createTreeCursor(); | ||
// --8<-- [start:example-using-cursor-with-names] | ||
let names: string[] = []; | ||
|
||
while (cursor.goToNextRuleWithKind(RuleKind.ContractDefinition)) { | ||
const innerCursor = cursor.spawn(); | ||
while (innerCursor.goToNext()) { | ||
const node = innerCursor.node(); | ||
const nodeName = innerCursor.nodeName; | ||
|
||
if (node.type == NodeType.Token && nodeName == FieldName.Name) { | ||
names.push(node.text); | ||
} | ||
} | ||
} | ||
|
||
expect(names).toEqual(["Foo", "Bar", "Baz"]); | ||
// --8<-- [end:example-using-cursor-with-names] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
- [Index](./index.md) | ||
- [How to use the CST Cursor](./how-to-use-the-cst-cursor/) | ||
- [How to parse a Solidity file](./how-to-parse-a-file/) |
72 changes: 72 additions & 0 deletions
72
documentation/public/user-guide/npm-package/how-to-use-the-cst-cursor/index.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Using a CST Cursor | ||
|
||
The CST (Concrete Syntax Tree) cursor is a powerful tool that allows you to traverse a CST in a depth-first search (DFS) pre-order fashion. | ||
This guide will walk you through the basics of using a CST cursor in your project. | ||
|
||
## Creating a Cursor | ||
|
||
First, you need to create an instance of the `Cursor` struct. This is done as follows: | ||
|
||
```{ .ts } | ||
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/cursor-api.ts:create-cursor" | ||
``` | ||
|
||
## Traversing the CST procedurally | ||
|
||
Once you have a cursor, you can use it to traverse the CST in the DFS order. The cursor provides several `goTo*` navigation functions | ||
for this purpose; each returning `true` if the cursor was successfully moved, and `false` otherwise. | ||
|
||
There are three main ways to do it: | ||
|
||
- according to the DFS order, i.e. next/previous nodes, | ||
- according to the relationship between the current node and the next node, e.g. siblings/children/parent, | ||
- according to the type of the next node, e.g. next token/rule. | ||
|
||
As such, the cursor is stateful and keeps track of the path it has taken through the CST. | ||
It starts at the root it was created at and is completed when it reaches its root when navigating forward. | ||
|
||
The below example uses a cursor to collect the names of all contracts in a source file, and returns them as a `Vec<String>`: | ||
|
||
```solidity title="input.sol" | ||
--8<-- "crates/solidity/outputs/cargo/tests/src/doc_examples/cursor_api/base.sol" | ||
``` | ||
|
||
```{ .ts } | ||
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/cursor-api.ts:example-list-contract-names" | ||
``` | ||
|
||
## Visiting only a sub-tree | ||
|
||
Sometimes, it's useful to only visit a sub-tree of the CST. In order to do that, we can use the `Cursor::spawn` function, | ||
which creates a new cursor that starts at the given node, not copying the previous path history. | ||
|
||
```solidity title="input.sol" | ||
--8<-- "crates/solidity/outputs/cargo/tests/src/doc_examples/cursor_api/base.sol" | ||
``` | ||
|
||
```{ .ts } | ||
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/cursor-api.ts:example-using-spawn" | ||
``` | ||
|
||
## Accessing the current node | ||
|
||
The `Cursor` struct provides several methods that allow you to access the currently visited node, its position in the source code | ||
and its ancestors. | ||
|
||
```solidity title="input.sol" | ||
--8<-- "crates/solidity/outputs/cargo/tests/src/doc_examples/cursor_api/node_accessors.sol" | ||
``` | ||
|
||
```{ .ts } | ||
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/cursor-api.ts:example-node-accessors" | ||
``` | ||
|
||
The CST children nodes are often named. Sometimes, it might be more convenient to lookup and access the node by its name, like so: | ||
|
||
```solidity title="input.sol" | ||
--8<-- "crates/solidity/outputs/cargo/tests/src/doc_examples/cursor_api/base.sol" | ||
``` | ||
|
||
```{ .ts } | ||
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/cursor-api.ts:example-using-cursor-with-names" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
- [Index](./index.md) | ||
- [How to use the CST Cursor](./how-to-use-the-cst-cursor/) | ||
- [How to parse a Solidity file](./how-to-parse-a-file/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.