From 0e3a46f8d445aaba468881305b421dabf89a7747 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 18:15:39 +0100 Subject: [PATCH] refactor(scripts): use native TypeScript AST helpers --- scripts/audit-seams.mjs | 67 +++------------- scripts/check-dynamic-import-warts.mjs | 81 +++++++------------- scripts/check-no-raw-window-open.mjs | 14 +--- scripts/check-plugin-sdk-subpath-exports.mjs | 8 +- scripts/lib/control-ui-i18n-raw-copy.ts | 25 ++---- scripts/lib/guard-inventory-utils.mjs | 4 +- scripts/report-test-temp-creations.mjs | 21 +---- 7 files changed, 47 insertions(+), 173 deletions(-) diff --git a/scripts/audit-seams.mjs b/scripts/audit-seams.mjs index aa74b2a2864..2416da640d0 100644 --- a/scripts/audit-seams.mjs +++ b/scripts/audit-seams.mjs @@ -9,8 +9,10 @@ import { BUNDLED_PLUGIN_PATH_PREFIX, BUNDLED_PLUGIN_ROOT_DIR, } from "./lib/bundled-plugin-paths.mjs"; +import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs"; import { optionalBundledClusterSet } from "./lib/optional-bundled-clusters.mjs"; import { escapeRegExp } from "./lib/regexp.mjs"; +import { toLine } from "./lib/ts-guard-utils.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const srcRoot = path.join(repoRoot, "src"); @@ -157,10 +159,6 @@ async function walkAllCodeFiles(rootDir, options = {}) { return out.toSorted((left, right) => normalizePath(left).localeCompare(normalizePath(right))); } -function toLine(sourceFile, node) { - return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; -} - function resolveRelativeSpecifier(specifier, importerFile) { if (!specifier.startsWith(".")) { return null; @@ -213,27 +211,9 @@ function collectPluginSdkImports(filePath, sourceFile) { }); } - function visit(node) { - if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { - push("import", node.moduleSpecifier, node.moduleSpecifier.text); - } else if ( - ts.isExportDeclaration(node) && - node.moduleSpecifier && - ts.isStringLiteral(node.moduleSpecifier) - ) { - push("export", node.moduleSpecifier, node.moduleSpecifier.text); - } else if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length === 1 && - ts.isStringLiteral(node.arguments[0]) - ) { - push("dynamic-import", node.arguments[0], node.arguments[0].text); - } - ts.forEachChild(node, visit); - } - - visit(sourceFile); + visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => { + push(kind, specifierNode, specifier); + }); return entries; } @@ -245,15 +225,7 @@ async function collectCorePluginSdkImports() { continue; } const source = await fs.readFile(filePath, "utf8"); - const scriptKind = - filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS; - const sourceFile = ts.createSourceFile( - filePath, - source, - ts.ScriptTarget.Latest, - true, - scriptKind, - ); + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); inventory.push(...collectPluginSdkImports(filePath, sourceFile)); } return inventory.toSorted(compareImports); @@ -284,20 +256,11 @@ function collectOptionalClusterStaticImports(filePath, sourceFile) { }); } - function visit(node) { - if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { - push("import", node.moduleSpecifier, node.moduleSpecifier.text); - } else if ( - ts.isExportDeclaration(node) && - node.moduleSpecifier && - ts.isStringLiteral(node.moduleSpecifier) - ) { - push("export", node.moduleSpecifier, node.moduleSpecifier.text); + visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => { + if (kind !== "dynamic-import") { + push(kind, specifierNode, specifier); } - ts.forEachChild(node, visit); - } - - visit(sourceFile); + }); return entries; } @@ -310,15 +273,7 @@ async function collectOptionalClusterStaticLeaks() { continue; } const source = await fs.readFile(filePath, "utf8"); - const scriptKind = - filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS; - const sourceFile = ts.createSourceFile( - filePath, - source, - ts.ScriptTarget.Latest, - true, - scriptKind, - ); + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); inventory.push(...collectOptionalClusterStaticImports(filePath, sourceFile)); } return inventory.toSorted((left, right) => { diff --git a/scripts/check-dynamic-import-warts.mjs b/scripts/check-dynamic-import-warts.mjs index bc42edfd6dc..35adcab82e9 100644 --- a/scripts/check-dynamic-import-warts.mjs +++ b/scripts/check-dynamic-import-warts.mjs @@ -14,64 +14,44 @@ import { const repoRoot = resolveRepoRoot(import.meta.url); const defaultRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")]; -function readStringLiteral(node) { - if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { - return node.text; - } - return null; -} - function isTypeOnlyImportDeclaration(node) { const clause = node.importClause; - if (!clause) { - return false; - } - if (clause.isTypeOnly) { - return true; - } - if (clause.name) { - return false; - } - const bindings = clause.namedBindings; - return ( - Boolean(bindings) && - ts.isNamedImports(bindings) && - bindings.elements.length > 0 && - bindings.elements.every((element) => element.isTypeOnly) + return Boolean( + clause && + (ts.isTypeOnlyImportDeclaration(clause) || + (!clause.name && + ts.isNamedImports(clause.namedBindings) && + clause.namedBindings.elements.length > 0 && + clause.namedBindings.elements.every(ts.isTypeOnlyImportOrExportDeclaration))), ); } function isTypeOnlyExportDeclaration(node) { - if (node.isTypeOnly === true) { - return true; - } const clause = node.exportClause; return ( - Boolean(clause) && - ts.isNamedExports(clause) && - clause.elements.length > 0 && - clause.elements.every((element) => element.isTypeOnly) + node.isTypeOnly === true || + Boolean( + clause && + ts.isNamedExports(clause) && + clause.elements.length > 0 && + clause.elements.every(ts.isTypeOnlyImportOrExportDeclaration), + ) ); } -function readDeclarationName(node) { +function isExecuteDeclaration(node) { if ( - (ts.isFunctionDeclaration(node) || - ts.isMethodDeclaration(node) || - ts.isVariableDeclaration(node)) && - node.name && - ts.isIdentifier(node.name) + !ts.isFunctionDeclaration(node) && + !ts.isMethodDeclaration(node) && + !ts.isVariableDeclaration(node) && + !ts.isPropertyAssignment(node) ) { - return node.name.text; + return false; } - - if (ts.isPropertyAssignment(node)) { - if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) { - return node.name.text; - } - } - - return null; + const name = ts.getNameOfDeclaration(node); + return Boolean( + name && (ts.isIdentifier(name) || ts.isStringLiteral(name)) && name.text === "execute", + ); } function isIgnoredTestHelperContent(content) { @@ -99,7 +79,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { const staticRuntimeImports = new Map(); const dynamicImports = new Map(); const directExecuteImports = []; - const declarationStack = []; const addLine = (map, specifier, line) => { const lines = map.get(specifier) ?? []; @@ -108,11 +87,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { }; const visit = (node) => { - const declarationName = readDeclarationName(node); - if (declarationName) { - declarationStack.push(declarationName); - } - if ( ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && @@ -135,11 +109,11 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0 ) { - const specifier = readStringLiteral(node.arguments[0]); + const specifier = ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null; if (specifier) { const line = toLine(sourceFile, node); addLine(dynamicImports, specifier, line); - if (declarationStack.includes("execute")) { + if (ts.findAncestor(node, isExecuteDeclaration)) { directExecuteImports.push({ line, reason: `direct dynamic import of "${specifier}" inside execute path; move it behind a cached loader`, @@ -149,9 +123,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { } ts.forEachChild(node, visit); - if (declarationName) { - declarationStack.pop(); - } }; visit(sourceFile); diff --git a/scripts/check-no-raw-window-open.mjs b/scripts/check-no-raw-window-open.mjs index 5f685796724..8f1239a4e8b 100644 --- a/scripts/check-no-raw-window-open.mjs +++ b/scripts/check-no-raw-window-open.mjs @@ -16,19 +16,9 @@ const repoRoot = resolveRepoRoot(import.meta.url); const uiSourceDir = path.join(repoRoot, "ui", "src", "ui"); const allowedCallsites = new Set([path.join(uiSourceDir, "open-external-url.ts")]); -function asPropertyAccess(expression) { - if (ts.isPropertyAccessExpression(expression)) { - return expression; - } - if (typeof ts.isPropertyAccessChain === "function" && ts.isPropertyAccessChain(expression)) { - return expression; - } - return null; -} - function isRawWindowOpenCall(expression) { - const propertyAccess = asPropertyAccess(unwrapExpression(expression)); - if (!propertyAccess || propertyAccess.name.text !== "open") { + const propertyAccess = unwrapExpression(expression); + if (!ts.isPropertyAccessExpression(propertyAccess) || propertyAccess.name.text !== "open") { return false; } diff --git a/scripts/check-plugin-sdk-subpath-exports.mjs b/scripts/check-plugin-sdk-subpath-exports.mjs index fe359c3e2e5..e22ae5dd773 100644 --- a/scripts/check-plugin-sdk-subpath-exports.mjs +++ b/scripts/check-plugin-sdk-subpath-exports.mjs @@ -71,13 +71,7 @@ async function collectViolations() { for (const filePath of files) { const sourceText = readFileSync(filePath, "utf8"); - const sourceFile = ts.createSourceFile( - filePath, - sourceText, - ts.ScriptTarget.Latest, - true, - filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, - ); + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); function push(kind, specifierNode, specifier) { const subpath = parsePluginSdkSubpath(specifier); diff --git a/scripts/lib/control-ui-i18n-raw-copy.ts b/scripts/lib/control-ui-i18n-raw-copy.ts index 97f2b01b7d7..32689c89391 100644 --- a/scripts/lib/control-ui-i18n-raw-copy.ts +++ b/scripts/lib/control-ui-i18n-raw-copy.ts @@ -49,16 +49,6 @@ function normalizeRawCopyText(raw: string): string { .trim(); } -function lineNumberForOffset(source: string, offset: number): number { - let line = 1; - for (let index = 0; index < offset && index < source.length; index += 1) { - if (source.charCodeAt(index) === 10) { - line += 1; - } - } - return line; -} - function parseDoubleQuotedString(raw: string): string { try { return JSON.parse(`"${raw}"`) as string; @@ -118,6 +108,7 @@ export function collectControlUiRawCopyFromSource(params: { const { filePath, source, sourceFile } = params; const repoPath = toRepoPath(filePath); const findings: RawCopyFinding[] = []; + const toLine = (offset: number) => sourceFile.getLineAndCharacterOfPosition(offset).line + 1; const staticAttrPattern = /\b(aria-label|placeholder|title)\s*=\s*"((?:(?!\$\{)[^"\\]|\\.)*?\p{L}(?:(?!\$\{)[^"\\]|\\.)*?)"/gu; for (const match of source.matchAll(staticAttrPattern)) { @@ -125,7 +116,7 @@ export function collectControlUiRawCopyFromSource(params: { if (rawText) { pushRawCopyFinding(findings, { kind: "html-attribute", - line: lineNumberForOffset(source, match.index ?? 0), + line: toLine(match.index ?? 0), name: match[1] ?? "attribute", path: repoPath, text: parseDoubleQuotedString(rawText), @@ -140,7 +131,7 @@ export function collectControlUiRawCopyFromSource(params: { if (rawText) { pushRawCopyFinding(findings, { kind: "object-property", - line: lineNumberForOffset(source, match.index ?? 0), + line: toLine(match.index ?? 0), name: match[1] ?? "property", path: repoPath, text: parseDoubleQuotedString(rawText), @@ -162,7 +153,7 @@ export function collectControlUiRawCopyFromSource(params: { ...node.template.templateSpans.map((span) => span.literal.text), ].join(INTERPOLATION_MARKER); } - const line = lineNumberForOffset(source, node.template.getStart(sourceFile)); + const line = toLine(node.template.getStart(sourceFile)); for (const match of logicalText.matchAll(attrPattern)) { const rawText = match[2]; if (rawText?.includes(INTERPOLATION_MARKER)) { @@ -199,13 +190,7 @@ async function collectFindings(): Promise { const findings: RawCopyFinding[] = []; for (const filePath of files.toSorted((left, right) => left.localeCompare(right))) { const source = await readFile(filePath, "utf8"); - const sourceFile = ts.createSourceFile( - filePath, - source, - ts.ScriptTarget.Latest, - true, - filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, - ); + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); findings.push(...collectControlUiRawCopyFromSource({ filePath, source, sourceFile })); } return findings; diff --git a/scripts/lib/guard-inventory-utils.mjs b/scripts/lib/guard-inventory-utils.mjs index bfeb365baaf..f81d9349456 100644 --- a/scripts/lib/guard-inventory-utils.mjs +++ b/scripts/lib/guard-inventory-utils.mjs @@ -239,10 +239,9 @@ export function formatGroupedInventoryHuman(params, inventory) { /** Parse TypeScript files and collect sorted inventory entries from each source file. */ export async function collectTypeScriptInventory(params) { const inventory = []; - const scriptKind = params.scriptKind ?? params.ts.ScriptKind.TS; for (const filePath of params.files) { - const cacheKey = `${scriptKind}:${filePath}`; + const cacheKey = filePath; let sourceFile = parsedTypeScriptSourceCache.get(cacheKey); if (!sourceFile) { let source = sourceTextCache.get(filePath); @@ -258,7 +257,6 @@ export async function collectTypeScriptInventory(params) { source, params.ts.ScriptTarget.Latest, true, - scriptKind, ); parsedTypeScriptSourceCache.set(cacheKey, sourceFile); } diff --git a/scripts/report-test-temp-creations.mjs b/scripts/report-test-temp-creations.mjs index 1c5cea9b473..b4527bf42f5 100644 --- a/scripts/report-test-temp-creations.mjs +++ b/scripts/report-test-temp-creations.mjs @@ -200,27 +200,8 @@ function isTempDirHelperImportSpec(filePath, specifier) { return stripKnownExtension(resolvedPath) === stripKnownExtension(TEMP_DIR_HELPER_PATH); } -function scriptKindForFile(filePath) { - if (/\.[cm]?tsx$/u.test(filePath)) { - return ts.ScriptKind.TSX; - } - if (/\.[cm]?jsx$/u.test(filePath)) { - return ts.ScriptKind.JSX; - } - if (/\.[cm]?js$/u.test(filePath)) { - return ts.ScriptKind.JS; - } - return ts.ScriptKind.TS; -} - function createSourceFile(filePath, sourceText) { - return ts.createSourceFile( - filePath, - sourceText, - ts.ScriptTarget.Latest, - true, - scriptKindForFile(filePath), - ); + return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); } function lineForNode(sourceFile, node) {