Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add How-to guides on parsing Solidity for CLI/Rust/NPM #716

Merged
merged 5 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"doxygen",
"ebnf",
"inheritdoc",
"instanceof",
"ipfs",
"mkdocs",
"napi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ use semver::Version;
use slang_solidity::kinds::{RuleKind, TokenKind};
use slang_solidity::language::Language;

const SOURCE: &str = "
contract Foo {}
contract Bar {}
contract Baz {}
";
const SOURCE: &str = include_str!("cursor_api.sol");

#[test]
fn using_cursor_api() -> Result<()> {
// --8<-- [start:example-list-contract-names]
let language = Language::new(Version::parse("0.8.0")?)?;
let parse_output = language.parse(RuleKind::SourceUnit, SOURCE);

Expand All @@ -29,6 +26,7 @@ fn using_cursor_api() -> Result<()> {
}

assert_eq!(contract_names, &["Foo", "Bar", "Baz"]);
// --8<-- [end:example-list-contract-names]
Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
contract Foo {}
contract Bar {}
contract Baz {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Language } from "@nomicfoundation/slang/language";
import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds";

test("list contract names", () => {
// ...or read a file using `fs.readFileSync` (or `fs.readFile`)
const data = "contract Foo {} contract Bar {} contract Baz {}";

const language = new Language("0.8.0");
const parseTree = language.parse(RuleKind.SourceUnit, data);

let contractNames = [];
let cursor = parseTree.createTreeCursor();

while (cursor.goToNextRuleWithKinds([RuleKind.ContractDefinition])) {
// You have to make sure you return the cursor to original position
cursor.goToFirstChild();
cursor.goToNextTokenWithKinds([TokenKind.Identifier]);

// The currently pointed-to node is the name of the contract
let tokenNode = cursor.node();
if (tokenNode.kind !== TokenKind.Identifier) {
throw new Error("Expected identifier");
}
contractNames.push(tokenNode.text);

cursor.goToParent();
}

expect(contractNames).toEqual(["Foo", "Bar", "Baz"]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pragma solidity ^0.8.0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// --8<-- [start:intro]
import { Language } from "@nomicfoundation/slang/language";
import { RuleKind } from "@nomicfoundation/slang/kinds";
import { Cursor } from "@nomicfoundation/slang/cursor";

const source = "int256 constant z = 1 + 2;";
const language = new Language("0.8.11");

let parseOutput = language.parse(RuleKind.SourceUnit, source);
// --8<-- [end:intro]

// --8<-- [start:step-1]
import * as path from "node:path";
import * as fs from "node:fs";

const data = fs.readFileSync(path.join(__dirname, "reconstruct-source.sol"), "utf8");

parseOutput = language.parse(RuleKind.SourceUnit, data);
let cursor: Cursor = parseOutput.createTreeCursor();
// --8<-- [end:step-1]
// --8<-- [start:step-2]
import { TokenNode } from "@nomicfoundation/slang/cst";

let output = "";
while (cursor.goToNext()) {
let node = cursor.node();
if (node instanceof TokenNode) {
output += node.text;
}
}

// --8<-- [end:step-2]
// We wrap this in a Jest test so that we can verify that the output is correct
test("reconstruct source", () => {
// --8<-- [start:step-2-assertion]
// Jest-style assertion for clarity
expect(output).toEqual("pragma solidity ^0.8.0;\n");
// --8<-- [end:step-2-assertion]
});
1 change: 1 addition & 0 deletions documentation/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ markdown_extensions:
- "pymdownx.snippets":
base_path: !ENV "REPO_ROOT"
check_paths: true
dedent_subsections: true
- "pymdownx.superfences"
- "pymdownx.tabbed":
alternate_style: true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
# How to parse a Solidity file

--8<-- "crates/solidity/inputs/language/snippets/under-construction.md"
In this guide, we'll walk you through the process of parsing a Solidity file using Slang. See [Installation](../#installation) on how to install Slang.

A file must be parsed according to a specific Solidity [version](../../../solidity-specification/supported-versions/). The version has to be explicitly specified and is not inferred from the source. To selectively parse parts of the source code using different versions, e.g. when the contract across multiple files has been flattened, you need to do that manually.

## Using the NPM package

Start by adding the Slang package as a dependency to your project:

```bash
npm install "@nomicfoundation/slang"
```

Using the API directly provides us with a more fine-grained control over the parsing process; we can parse individual rules like contracts, various definitions or even expressions.

We start by creating a `Language` struct with a given version. This is an entry point for our parser API.

```ts
import { Language } from "@nomicfoundation/slang/language";
import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds";
import { Cursor } from "@nomicfoundation/slang/cursor";

const source = "int256 constant z = 1 + 2;";
const language = new Language("0.8.11");

const parseOutput = language.parse(RuleKind.SourceUnit, source);
const cursor: Cursor = parseOutput.createTreeCursor();
```

The resulting `ParseOutput` class exposes these helpful functions:

- `errors()/isValid()` that return structured parse errors, if any,
- `tree()` that gives us back a CST (partial if there were parse errors),
- `fn createTreeCursor()` that creates a `Cursor` type used to conveniently walk the parse tree.

### Example 1: Reconstruct the Solidity file

Let's try the same example, only now using the API directly.

We'll start with this file:

```solidity title="reconstruct-source.sol"
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/reconstruct-source.sol"
```

#### Step 1: Parse the Solidity file

Let's naively (ignore the errors) read the file and parse it:

```{ .ts }
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/reconstruct-source.ts:step-1"
```

#### Step 2: Reconstruct the source code

The `Cursor` visits the tree nodes in a depth-first search (DFS) fashion. Since our CST is complete (includes trivia such as whitespace), it's enough to visit the `Token` nodes and concatenate their text to reconstruct the original source code.

Let's do that:

```{ .ts }
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/reconstruct-source.ts:step-2"
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/reconstruct-source.ts:step-2-assertion"
```

### Example 2: List the top-level contracts and their names

The `Cursor` type provides procedural-style functions that allow you to navigate the source in a step-by-step manner. In addition to `goToNext`, we can go to the parent, first child, next sibling, etc., as well as nodes with a given kind.

To list the top-level contracts and their names, we need to visit the `ContractDefinition` rule nodes and then their `Identifier` children.

Let's do that:

```{ .ts }
--8<-- "crates/solidity/outputs/npm/tests/src/doc-examples/list-contract-names.ts"
```
Loading
Loading