From 37c177b0eb8deff0038c2b28b751ffb95c924241 Mon Sep 17 00:00:00 2001 From: Vivswan Shah <58091053+Vivswan@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:05:52 -0400 Subject: [PATCH] refactor(scripts): walk the architecture lint's AST with estree-walker arch-lint.ts walked the oxc-parser tree with a hand-rolled recursive generator, nodesOf, that descended every object and array for anything carrying a string `type`. estree-walker, already a declared devDependency, does that walk: it ducktypes children by their `type` field, so oxc's TypeScript nodes walk like the ESTree ones. Only its estree-typed signature needs a cast, at the one call. The differential check ran the old walk, importSpecifiers, and lintArchitecture against the new over every .ts file under src/, test/, and .github/scripts/ (536 files, 584,305 nodes in visit order) and over 5,000 seeded generated modules covering every import form the tests list, in nested positions, with computed specifiers and non-parsing files. Node sequences, specifier lists, and refusal messages were identical throughout. lintArchitecture gives the same empty verdict on the repository, and the same single forbidden-import line on a scratch copy of src/ with an undeclared text -> problem import injected. --- .github/scripts/arch-lint.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/scripts/arch-lint.ts b/.github/scripts/arch-lint.ts index 011b7aa1..fb3ba195 100644 --- a/.github/scripts/arch-lint.ts +++ b/.github/scripts/arch-lint.ts @@ -11,8 +11,9 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { inspect } from "node:util"; +import { type Node as ESTreeNode, walk } from "estree-walker"; import { err, ok, Result } from "neverthrow"; -import { type Node, parseSync } from "oxc-parser"; +import { type Node, type Program, parseSync } from "oxc-parser"; import { parseDocument } from "yaml"; import { z } from "zod"; import { countNoun, quote } from "../../src/text.js"; @@ -133,19 +134,10 @@ function layerOf(arch: Architecture, path: string): string | undefined { )?.[0]; } -function* nodesOf(value: unknown): Generator { - if (Array.isArray(value)) { - for (const item of value) { - yield* nodesOf(item); - } - } else if (typeof value === "object" && value !== null) { - if ("type" in value && typeof value.type === "string") { - yield value as Node; - } - for (const child of Object.values(value)) { - yield* nodesOf(child); - } - } +/** `visit` on every node of `program`, preorder. estree-walker finds the children by the fields carrying a `type`, + * so oxc's TypeScript nodes walk like the ESTree ones; only its signature is typed to ESTree, hence the casts. */ +function eachNode(program: Program, visit: (node: Node) => void): void { + walk(program as unknown as ESTreeNode, { enter: (node) => visit(node as unknown as Node) }); } /** The expression under the wrappers parentheses and TypeScript add: `(require)(x)`, `require!(x)`, @@ -222,9 +214,9 @@ export function importSpecifiers(text: string, file: string): string[] { ); } const specifiers = new Set(); - for (const node of nodesOf(program)) { + eachNode(program, (node) => { if (!isModuleLoad(node)) { - continue; + return; } const specifier = literalModuleRequest(node); if (specifier === undefined) { @@ -233,7 +225,7 @@ export function importSpecifiers(text: string, file: string): string[] { ); } specifiers.add(specifier); - } + }); return [...specifiers].filter((specifier) => /^\.\.?\//.test(specifier)); }