6.2. Executing Queries#
Queries are executed starting from any cursor that you can create with the Parser and Cursor APIs. Usually you'll run a query from either the root of the parse result parseOutput.createTreeCursor()
or from a cursor created with cursor.spawn()
to restrict the search to a given sub-tree.
Creating and executing queries#
You can create a Query
object using Query.create()
, which accepts a string value. These can be then used by cursor.query()
to execute it.
You can pass multiple queries to a cursor to efficiently traverse the tree looking for matches. They will be executed simultaneously, returning matches in the order they appear in the input.
import assert from "node:assert";
import { Parser } from "@nomicfoundation/slang/parser";
import { Query, QueryMatchIterator } from "@nomicfoundation/slang/cst";
export function executeQueries(soliditySource: string, queries: Query[]): QueryMatchIterator {
const parser = Parser.create("0.8.28");
const parseOutput = parser.parseFileContents(soliditySource);
assert(parseOutput.isValid());
return parseOutput.createTreeCursor().query(queries);
}
Iterating over node patterns#
Queries allow you to iterate over all node patterns that match the query, which can replace your need for manual iteration via cursors or visitors. Each match has a cursor that points to the node matched by the outermost pattern in the query. This is called the match root.
Let's use this to list all the contract definitions in a source file:
import assert from "node:assert";
import { executeQueries } from "./common.mjs";
import { Query } from "@nomicfoundation/slang/cst";
test("match roots", () => {
const matches = executeQueries(
`
contract Foo {}
contract Bar {}
contract Baz {}
`,
[
Query.create(`
[ContractDefinition]
`),
],
);
const found = [];
for (const match of matches) {
found.push(match.root.node.unparse().trim());
}
assert.deepStrictEqual(found, ["contract Foo {}", "contract Bar {}", "contract Baz {}"]);
});
Capturing nodes by name#
You can also capture specific nodes in the query by name, and get a cursor to each of them:
Let's use this to list all the contract names:
import assert from "node:assert";
import { executeQueries } from "./common.mjs";
import { Query } from "@nomicfoundation/slang/cst";
test("match captures", () => {
const matches = executeQueries(
`
contract Foo {}
contract Bar {}
contract Baz {}
`,
[
Query.create(`
[ContractDefinition
@name name: [Identifier]
]
`),
],
);
const found = [];
for (const match of matches) {
const names = match.captures["name"];
assert.strictEqual(names.length, 1);
found.push(names[0].node.unparse());
}
assert.deepStrictEqual(found, ["Foo", "Bar", "Baz"]);
});
Detecting Query errors#
If there is a mistake in your query, for example, if you use an invalid node kind, you will get a QueryError
exception. The error will contain a message to indicate what went wrong, and the text range in the query where the error occurred.
import assert from "node:assert";
import { Query, QueryError } from "@nomicfoundation/slang/cst";
test("query errors", () => {
try {
Query.create(`
[NonExistingNode]
`);
} catch (error) {
const queryError = error as QueryError;
assert.strictEqual(
queryError.message.trim(),
`Parse error:\n'NonExistingNode' is not a valid node kind at: NonExistingNode]`,
);
assert.deepStrictEqual(queryError.textRange, {
start: { line: 1, column: 7, utf8: 8, utf16: 8 },
end: { line: 2, column: 4, utf8: 29, utf16: 29 },
});
}
});
Multiple queries simultaneously#
We can also execute multiple queries simultaneously, which will return all the matches as they are found in the tree. This can be useful when you want to match multiple types of nodes in a single pass. Results will be reported in order, and each will have an index that can be used to identify which query is matched.
import assert from "node:assert";
import { Query } from "@nomicfoundation/slang/cst";
import { executeQueries } from "./common.mjs";
test("multiple queries", () => {
const matches = executeQueries(
`
struct Foo { uint a; }
enum Bar { A, B }
struct Baz { uint b; uint c; }
enum Qux { C, D }
`,
[
Query.create(`
[StructDefinition
@name [Identifier]
]
`),
Query.create(`
[EnumDefinition
@name [Identifier]
]
`),
],
);
const found = [];
for (const match of matches) {
const names = match.captures["name"];
assert.strictEqual(names.length, 1);
found.push({
queryIndex: match.queryIndex,
name: names[0].node.unparse(),
});
}
assert.deepStrictEqual(found, [
{ queryIndex: 0, name: "Foo" },
{ queryIndex: 1, name: "Bar" },
{ queryIndex: 0, name: "Baz" },
{ queryIndex: 1, name: "Qux" },
]);
});