Add childrenIter method to API nodes (#64302)

This commit is contained in:
Wesley Wigham authored and GitHub committed 2026-09-18 18:09:02 +00:00
1 parent c975de5011
commit 5bb0fe6987
8 files changed
+2716 -5

No files matched your search

@@ -69,6 +69,10 @@ export class RemoteNodeList extends Array<RemoteNode> implements NodeArray<Remot
return this.view.getUint32(this._byteIndex + NODE_OFFSET_NEXT, true);
}
get firstNodeIndex(): number {
return this.index + 1;
}
private get data(): number {
return this.view.getUint32(this._byteIndex + NODE_OFFSET_DATA, true);
}
@@ -200,7 +204,7 @@ export class RemoteNodeList extends Array<RemoteNode> implements NodeArray<Remot
return this.getOrCreateChildAtNodeIndex(next) as RemoteNode;
}
private getOrCreateChildAtNodeIndex(index: number): RemoteNode | RemoteNodeList {
getOrCreateChildAtNodeIndex(index: number): RemoteNode | RemoteNodeList {
let child = this.sourceFile.nodes[index];
if (!child) {
const kind = this.view.getUint32(this.sourceFile._offsetNodes + index * NODE_LEN + NODE_OFFSET_KIND, true);
@@ -271,6 +275,36 @@ export class RemoteNode extends RemoteNodeBase implements Node {
}
}
*childrenIter<TNext = void>(): Generator<Node, TNext | undefined, TNext> {
if (this.hasChildren()) {
let next = this.index + 1;
do {
const child = this.getOrCreateChildAtNodeIndex(next);
if (child instanceof RemoteNodeList) {
if (child.length) {
let listNext = child.firstNodeIndex;
while (listNext) {
const node = child.getOrCreateChildAtNodeIndex(listNext) as RemoteNode;
listNext = node.next;
const result = yield node;
if (result) {
return result;
}
}
}
}
else if (child.kind !== SyntaxKind.JSDoc) {
const result = yield child;
if (result) {
return result;
}
}
next = child.next;
}
while (next);
}
}
get jsDoc(): readonly Node[] | undefined {
if (!this.hasChildren()) {
return undefined;
+1
View File
@@ -90,6 +90,7 @@ export interface Node extends ReadonlyTextRange {
readonly flags: NodeFlags;
readonly parent: Node;
readonly jsDoc?: readonly Node[] | undefined;
childrenIter<TNext = void>(): Generator<Node, TNext | undefined, TNext>;
forEachChild<T>(visitor: (node: Node) => T, visitArray?: (nodes: NodeArray<Node>) => T): T | undefined;
getSourceFile(): SourceFile;
getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number;
File diff suppressed because it is too large. Load diff
+37
View File
@@ -59,6 +59,43 @@ function forEachChildOfJSDocParameterOrPropertyTag<T>(data: any, cbNode: (node:
export { forEachChildOfJSDocParameterOrPropertyTag as forEachChildOfJSDocParameterTag, forEachChildOfJSDocParameterOrPropertyTag as forEachChildOfJSDocPropertyTag };
// ── yieldEachChild implementations ──
function* yieldEachChildOfJSDocParameterOrPropertyTag<T>(data: any): Generator<Node, T | undefined, T> {
if (data.tagName) {
const res = yield data.tagName;
if (res) return res;
}
if (data.isNameFirst) {
if (data.name) {
const res = yield data.name;
if (res) return res;
}
if (data.typeExpression) {
const res = yield data.typeExpression;
if (res) return res;
}
}
else {
if (data.typeExpression) {
const res = yield data.typeExpression;
if (res) return res;
}
if (data.name) {
const res = yield data.name;
if (res) return res;
}
}
if (data.comment) {
for (const node of data.comment) {
const res = yield node;
if (res) return res;
}
}
}
export { yieldEachChildOfJSDocParameterOrPropertyTag as yieldEachChildOfJSDocParameterTag, yieldEachChildOfJSDocParameterOrPropertyTag as yieldEachChildOfJSDocPropertyTag };
// ── visitEachChild implementations ──
function visitEachChildOfJSDocParameterOrPropertyTag(node: JSDocParameterOrPropertyTag, visitor: Visitor): JSDocParameterOrPropertyTag {
+218
View File
@@ -0,0 +1,218 @@
// Benchmarks Node.childrenIter against Node.forEachChild for local factory nodes
// and RemoteNodes fetched through the sync API.
//
// Run with:
// npm run node -- test/sync/ast.bench.ts
// npm run node -- test/sync/ast.bench.ts --filter "remote recursive"
import type {
Block,
Identifier,
Node,
} from "@typescript/typescript/unstable/ast";
import {
isBlock,
isIdentifier,
} from "@typescript/typescript/unstable/ast";
import {
createBlock,
createExpressionStatement,
createIdentifier,
createIfStatement,
} from "@typescript/typescript/unstable/ast/factory";
import { createVirtualFileSystem } from "@typescript/typescript/unstable/fs";
import { API } from "@typescript/typescript/unstable/sync";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { Bench } from "tinybench";
const treeDepth = 5;
const expressionsPerBlock = 6;
const earlyTargetName = "earlyTargetIdentifier";
const midpointTargetName = "midpointTargetIdentifier";
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
if (isMain) {
const { values } = parseArgs({
options: {
filter: { type: "string" },
singleIteration: { type: "boolean", default: false },
},
});
runBenchmarks(values);
}
interface Tree {
node: Block;
source: string;
}
function createTree(depth = treeDepth, path = "root"): Tree {
const statements = [];
const sourceStatements: string[] = [];
for (let i = 0; i < expressionsPerBlock; i++) {
const name = path === "root" && i === 0
? earlyTargetName
: path === "root_1" && i === 0
? midpointTargetName
: `value_${path}_${i}`;
statements.push(createExpressionStatement(createIdentifier(name)));
sourceStatements.push(`${name};`);
}
if (depth > 0) {
const thenTree = createTree(depth - 1, `${path}_0`);
const elseTree = createTree(depth - 1, `${path}_1`);
const conditionName = `condition_${path}`;
statements.push(createIfStatement(
createIdentifier(conditionName),
thenTree.node,
elseTree.node,
));
sourceStatements.push(`if (${conditionName}) ${thenTree.source} else ${elseTree.source}`);
}
return {
node: createBlock(statements),
source: `{${sourceStatements.join("\n")}}`,
};
}
function collectWithForEachChild(node: Node): Node[] {
const children: Node[] = [];
node.forEachChild(child => {
children.push(child);
return undefined;
});
return children;
}
function collectWithChildrenIter(node: Node): Node[] {
const children: Node[] = [];
for (const child of node.childrenIter()) {
children.push(child);
}
return children;
}
function walkWithForEachChild(node: Node): number {
let count = 1;
node.forEachChild(child => {
count += walkWithForEachChild(child);
});
return count;
}
function walkWithChildrenIter(node: Node): number {
let count = 1;
for (const child of node.childrenIter()) {
count += walkWithChildrenIter(child);
}
return count;
}
function findWithForEachChild(node: Node, targetName: string): Identifier | undefined {
return node.forEachChild(child => {
if (isIdentifier(child) && child.text === targetName) return child;
return findWithForEachChild(child, targetName);
});
}
function findWithChildrenIter(node: Node, targetName: string): Identifier | undefined {
for (const child of node.childrenIter()) {
if (isIdentifier(child) && child.text === targetName) return child;
const result = findWithChildrenIter(child, targetName);
if (result) return result;
}
return undefined;
}
export function runBenchmarks(options?: { filter?: string; singleIteration?: boolean; }): void {
const { filter, singleIteration } = options ?? {};
const localTree = createTree();
const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url).toString());
const configPath = "/tsconfig.json";
const sourcePath = "/src/bench.ts";
using api = new API({
cwd: repoRoot,
tsserverPath: fileURLToPath(new URL(`../../../../built/local/tsc${process.platform === "win32" ? ".exe" : ""}`, import.meta.url).toString()),
fs: createVirtualFileSystem({
[configPath]: "{}",
[sourcePath]: localTree.source,
}),
});
const snapshot = api.createSnapshot({ openProject: configPath });
const remoteSourceFile = snapshot.getConfiguredProject(configPath)!.program.getSourceFile(sourcePath)!;
assert.ok(remoteSourceFile.statements);
const remoteTree = remoteSourceFile.statements[0];
assert.ok(isBlock(remoteTree));
assert.deepStrictEqual(
collectWithChildrenIter(localTree.node).map(node => node.kind),
collectWithForEachChild(localTree.node).map(node => node.kind),
);
assert.strictEqual(walkWithChildrenIter(localTree.node), walkWithForEachChild(localTree.node));
assert.strictEqual(walkWithChildrenIter(remoteTree), walkWithForEachChild(remoteTree));
assert.strictEqual(walkWithChildrenIter(localTree.node), walkWithChildrenIter(remoteTree));
for (const targetName of [earlyTargetName, midpointTargetName]) {
assert.strictEqual(findWithChildrenIter(localTree.node, targetName)?.text, targetName);
assert.strictEqual(findWithForEachChild(remoteTree, targetName)?.text, targetName);
}
const bench = new Bench({
name: "AST child traversal",
iterations: 30,
warmupIterations: 10,
...singleIteration ? {
iterations: 1,
warmup: false,
time: 0,
} : undefined,
});
const isAsync = false;
for (const [nodeType, node] of [["factory", localTree.node], ["remote", remoteTree]] as const) {
bench
.add(`${nodeType} immediate children - forEachChild`, () => {
assert.strictEqual(collectWithForEachChild(node).length, node.statements.length);
}, { async: isAsync })
.add(`${nodeType} immediate children - childrenIter`, () => {
assert.strictEqual(collectWithChildrenIter(node).length, node.statements.length);
}, { async: isAsync })
.add(`${nodeType} recursive walk - forEachChild`, () => {
assert.ok(walkWithForEachChild(node) > 1);
}, { async: isAsync })
.add(`${nodeType} recursive walk - childrenIter`, () => {
assert.ok(walkWithChildrenIter(node) > 1);
}, { async: isAsync })
.add(`${nodeType} recursive early exit - forEachChild`, () => {
assert.strictEqual(findWithForEachChild(node, earlyTargetName)?.text, earlyTargetName);
}, { async: isAsync })
.add(`${nodeType} recursive early exit - childrenIter`, () => {
assert.strictEqual(findWithChildrenIter(node, earlyTargetName)?.text, earlyTargetName);
}, { async: isAsync })
.add(`${nodeType} recursive midpoint exit - forEachChild`, () => {
assert.strictEqual(findWithForEachChild(node, midpointTargetName)?.text, midpointTargetName);
}, { async: isAsync })
.add(`${nodeType} recursive midpoint exit - childrenIter`, () => {
assert.strictEqual(findWithChildrenIter(node, midpointTargetName)?.text, midpointTargetName);
}, { async: isAsync });
}
if (filter) {
const pattern = filter.toLowerCase();
for (const task of [...bench.tasks]) {
if (!task.name.toLowerCase().includes(pattern)) {
bench.remove(task.name);
}
}
}
bench.runSync();
for (const task of bench.tasks) {
if (task.result && "error" in task.result) throw task.result.error;
}
console.table(bench.table());
}
+83 -3
View File
@@ -31,6 +31,7 @@ import {
createExpressionStatement,
createIdentifier,
createIfStatement,
createMissingDeclaration,
createNodeArray,
createNumericLiteral,
createSourceFile,
@@ -44,13 +45,18 @@ import {
visitNodes,
} from "@typescript/typescript/unstable/ast/visitor";
import { createVirtualFileSystem } from "@typescript/typescript/unstable/fs";
import { API } from "@typescript/typescript/unstable/sync";
import {
API,
Checker,
TypeFlags,
} from "@typescript/typescript/unstable/sync";
import assert from "node:assert";
import {
describe,
test,
} from "node:test";
import { fileURLToPath } from "node:url";
import { runBenchmarks } from "./ast.bench.ts";
// ---------------------------------------------------------------------------
// Helpers
@@ -65,6 +71,17 @@ function collectKinds(node: Node): SyntaxKind[] {
return kinds;
}
describe("NodeObject + childrenIter", () => {
test("skips absent optional child lists", () => {
const node = createMissingDeclaration();
assert.deepStrictEqual([...node.childrenIter()], []);
});
});
test("Benchmarks", async () => {
await runBenchmarks({ singleIteration: true });
});
// ---------------------------------------------------------------------------
// cloneNode
// ---------------------------------------------------------------------------
@@ -575,10 +592,14 @@ function spawnAPI(files: Record<string, string> = {
});
}
function getRemoteSourceFile(api: API, configPath: string, filePath: string) {
function getRemoteSourceFileAndChecker(api: API, configPath: string, filePath: string) {
const snapshot = api.createSnapshot({ openProject: configPath });
const project = snapshot.getConfiguredProject(configPath)!;
return project.program.getSourceFile(filePath)!;
return [project.program.getSourceFile(filePath)!, project.checker] as const;
}
function getRemoteSourceFile(api: API, configPath: string, filePath: string) {
return getRemoteSourceFileAndChecker(api, configPath, filePath)[0];
}
describe("RemoteNode + cloneNode", () => {
@@ -1051,6 +1072,17 @@ describe("RemoteNode + child/token getters", () => {
}
}
async function withFirstStatementAsync(source: string, fn: (stmt: Node, sf: SourceFile, api: API<false>, checker: Checker) => Promise<void>) {
const api = spawnAPI({ "/tsconfig.json": "{}", "/src/children.ts": source });
try {
const [sf, checker] = getRemoteSourceFileAndChecker(api, "/tsconfig.json", "/src/children.ts");
await fn(sf.statements[0], sf, api, checker);
}
finally {
api.close();
}
}
function findFirstOfKind(node: Node, kind: SyntaxKind): Node | undefined {
let found: Node | undefined;
const walk = (n: Node): undefined => {
@@ -1065,6 +1097,54 @@ describe("RemoteNode + child/token getters", () => {
return found;
}
test("childrenIter generates all node children and supports early return value passthru", () => {
withFirstStatement("if (x) {}", stmt => {
const texts: string[] = [];
for (const c of stmt.childrenIter()) {
texts.push(c.getText());
}
assert.deepStrictEqual(texts, ["x", "{}"]);
const cGen = stmt.childrenIter<Node | undefined>();
let state = cGen.next();
while (!state.done) {
const n = state.value;
let foundX: Node | undefined;
if (n.getText() === "x") {
foundX = n;
}
state = cGen.next(foundX);
}
assert.strictEqual(
state.value,
stmt.forEachChild(n => {
return n;
}),
);
});
});
test("childrenIter works with async generators", async () => {
await withFirstStatementAsync("class X { p: any }", async (stmt, sf, api, checker) => {
async function* visitNodeForFirstAnyChild(node: Node): AsyncGenerator<Node | undefined, Node | undefined, Node | undefined> {
for (const n of node.childrenIter()) {
const t = await checker.getTypeAtLocation(n);
if (t.flags & TypeFlags.Any) return n;
const res = yield* visitNodeForFirstAnyChild(n);
if (res) return res;
}
}
const res = (await visitNodeForFirstAnyChild(stmt).next()).value;
assert.strictEqual(res!.getText(), "p: any");
});
});
test("childrenIter skips empty NodeArrays", () => {
withFirstStatement("function f() {}", stmt => {
const body = findFirstOfKind(stmt, SyntaxKind.Block)!;
assert.deepStrictEqual([...body.childrenIter()], []);
});
});
test("getChildren materializes the punctuation/keyword tokens the AST omits", () => {
withFirstStatement("if (x) {}", stmt => {
const texts = stmt.getChildren().map(c => c.getText());
+35 -1
View File
@@ -1544,6 +1544,10 @@ function emitRemoteNodeList(w: CodeWriter) {
w.write(` return this.view.getUint32(this._byteIndex + NODE_OFFSET_NEXT, true);`);
w.write(` }`);
w.write(``);
w.write(` get firstNodeIndex(): number {`);
w.write(` return this.index + 1;`);
w.write(` }`);
w.write(``);
w.write(` private get data(): number {`);
w.write(` return this.view.getUint32(this._byteIndex + NODE_OFFSET_DATA, true);`);
w.write(` }`);
@@ -1635,7 +1639,7 @@ function emitRemoteNodeList(w: CodeWriter) {
w.write(` return this.getOrCreateChildAtNodeIndex(next) as RemoteNode;`);
w.write(` }`);
w.write(``);
w.write(` private getOrCreateChildAtNodeIndex(index: number): RemoteNode | RemoteNodeList {`);
w.write(` getOrCreateChildAtNodeIndex(index: number): RemoteNode | RemoteNodeList {`);
w.write(` let child = this.sourceFile.nodes[index];`);
w.write(` if (!child) {`);
w.write(` const kind = this.view.getUint32(this.sourceFile._offsetNodes + index * NODE_LEN + NODE_OFFSET_KIND, true);`);
@@ -1708,6 +1712,36 @@ function emitRemoteNodeClassOpen(w: CodeWriter) {
w.write(` }`);
w.write(` }`);
w.write(``);
w.write(` *childrenIter<TNext = void>(): Generator<Node, TNext | undefined, TNext> {`);
w.write(` if (this.hasChildren()) {`);
w.write(` let next = this.index + 1;`);
w.write(` do {`);
w.write(` const child = this.getOrCreateChildAtNodeIndex(next);`);
w.write(` if (child instanceof RemoteNodeList) {`);
w.write(` if (child.length) {`);
w.write(` let listNext = child.firstNodeIndex;`);
w.write(` while (listNext) {`);
w.write(` const node = child.getOrCreateChildAtNodeIndex(listNext) as RemoteNode;`);
w.write(` listNext = node.next;`);
w.write(` const result = yield node;`);
w.write(` if (result) {`);
w.write(` return result;`);
w.write(` }`);
w.write(` }`);
w.write(` }`);
w.write(` }`);
w.write(` else if (child.kind !== SyntaxKind.JSDoc) {`);
w.write(` const result = yield child;`);
w.write(` if (result) {`);
w.write(` return result;`);
w.write(` }`);
w.write(` }`);
w.write(` next = child.next;`);
w.write(` }`);
w.write(` while (next);`);
w.write(` }`);
w.write(` }`);
w.write(``);
w.write(` get jsDoc(): readonly Node[] | undefined {`);
w.write(` if (!this.hasChildren()) {`);
w.write(` return undefined;`);
+75
View File
@@ -679,6 +679,7 @@ function generateFactory(): string {
if (members.filter(m => m.isChild()).length > 0) {
for (const kind of node.kindTypes()) {
handWrittenForEachChildImports.push(`forEachChildOf${kind.name}`);
handWrittenForEachChildImports.push(`yieldEachChildOf${kind.name}`);
}
}
}
@@ -721,6 +722,11 @@ function generateFactory(): string {
out.push(` return fn ? fn(this._data, visitor, visitArray) : undefined;`);
out.push(` }`);
out.push(``);
out.push(` childrenIter<TNext = void>(): Generator<Node, TNext | undefined, TNext> {`);
out.push(` const fn = yieldEachChildTable[this.kind];`);
out.push(` return fn ? fn(this._data) : emptyChildrenIter();`);
out.push(` }`);
out.push(``);
out.push(` getSourceFile(): SourceFile {`);
out.push(` let node: Node = this as unknown as Node;`);
out.push(` while (node.parent) node = node.parent;`);
@@ -901,6 +907,75 @@ function generateFactory(): string {
out.push(`};`);
out.push(``);
// ── yieldEachChildTable ──
out.push(`type YieldEachChildFunction = <T>(data: any) => Generator<Node, T | undefined, T>;`);
out.push(``);
out.push(`function* emptyChildrenIter<T>(): Generator<Node, T | undefined, T> {`);
out.push(` return undefined;`);
out.push(`}`);
out.push(``);
out.push(`const yieldEachChildTable: Record<number, YieldEachChildFunction> = {`);
// Schema nodes
for (const node of api.nodes()) {
if (node.handWritten) continue;
if (isVariantNode(node)) continue;
const members = tsMembers(node);
const childMembers = members.filter(m => m.isChild());
if (childMembers.length === 0) continue;
const syntaxKinds = node.allKinds();
if (node.handWrittenVisitor) {
for (const sk of syntaxKinds) {
out.push(` [${sk.formatTypeScript()}]: yieldEachChildOf${node.name},`);
}
continue;
}
const visits = childMembers.map(m => {
const propName = api.uncapitalize(m.name);
const listKind = m.listKind;
if (listKind) {
const visit = `for (const n of data.${propName}) { const res = yield n; if (res) return res; }`;
return m.optional ? `if (data.${propName}) { ${visit} }` : visit;
}
return `if (data.${propName}) { const res = yield data.${propName}; if (res) return res; }`;
});
const body = visits.join("\n ");
for (const sk of syntaxKinds) {
out.push(` [${sk.formatTypeScript()}]: function* (data) {`);
out.push(` ${body}`);
out.push(` },`);
}
}
// Variant nodes
for (const v of tsVariants) {
if (!v.members) continue;
const childMembers = v.members.filter(m => m.type.baseKind() === "node" || m.type.baseKind() === "list");
if (childMembers.length === 0) continue;
if (v.handWrittenVisitor) {
out.push(` [SyntaxKind.${v.syntaxKind}]: yieldEachChildOf${v.tsName},`);
continue;
}
const visits = childMembers.map(m => {
const propName = api.uncapitalize(m.name);
const listKind = m.listKind;
if (listKind) {
const visit = `for (const n of data.${propName}) { const res = yield n; if (res) return res; }`;
return m.optional ? `if (data.${propName}) { ${visit} }` : visit;
}
return `if (data.${propName}) { const res = yield data.${propName}; if (res) return res; }`;
});
const body = visits.join("\n ");
out.push(` [SyntaxKind.${v.syntaxKind}]: function* (data) {`);
out.push(` ${body}`);
out.push(` },`);
}
// SourceFile is handWritten so we add its yieldEachChild entry manually
out.push(` [SyntaxKind.SourceFile]: function* (data) {`);
out.push(` for (const n of data.statements) { const res = yield n; if (res) return res; }`);
out.push(` const res = yield data.endOfFileToken; if (res) return res;`);
out.push(` },`);
out.push(`};`);
out.push(``);
// ── visitNode / visitNodes ──
out.push(`function visitNode<T>(cbNode: (node: Node) => T, node: Node | undefined): T | undefined {`);
out.push(` return node ? cbNode(node) : undefined;`);