perf(test): replace temp-path guard AST parse with fast scanner

This commit is contained in:
Peter Steinberger
2026-02-22 17:42:40 +00:00
parent 2ed94a08c0
commit 90a8ddc3c6

View File

@@ -1,7 +1,6 @@
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import ts from "typescript";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const RUNTIME_ROOTS = ["src", "extensions"]; const RUNTIME_ROOTS = ["src", "extensions"];
@@ -9,78 +8,181 @@ const SKIP_PATTERNS = [
/\.test\.tsx?$/, /\.test\.tsx?$/,
/\.test-helpers\.tsx?$/, /\.test-helpers\.tsx?$/,
/\.test-utils\.tsx?$/, /\.test-utils\.tsx?$/,
/\.test-harness\.tsx?$/,
/\.e2e\.tsx?$/, /\.e2e\.tsx?$/,
/\.d\.ts$/, /\.d\.ts$/,
/[\\/](?:__tests__|tests)[\\/]/, /[\\/](?:__tests__|tests)[\\/]/,
/[\\/][^\\/]*test-helpers(?:\.[^\\/]+)?\.ts$/, /[\\/][^\\/]*test-helpers(?:\.[^\\/]+)?\.ts$/,
/[\\/][^\\/]*test-utils(?:\.[^\\/]+)?\.ts$/,
/[\\/][^\\/]*test-harness(?:\.[^\\/]+)?\.ts$/,
]; ];
function shouldSkip(relativePath: string): boolean { function shouldSkip(relativePath: string): boolean {
return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath)); return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath));
} }
function isIdentifierNamed(node: ts.Node, name: string): node is ts.Identifier { function stripCommentsForScan(input: string): string {
return ts.isIdentifier(node) && node.text === name; return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
} }
function isPathJoinCall(expr: ts.LeftHandSideExpression): boolean { function findMatchingParen(source: string, openIndex: number): number {
return ( let depth = 1;
ts.isPropertyAccessExpression(expr) && let quote: "'" | '"' | "`" | null = null;
expr.name.text === "join" && let escaped = false;
isIdentifierNamed(expr.expression, "path") for (let i = openIndex + 1; i < source.length; i += 1) {
); const ch = source[i];
if (quote) {
if (escaped) {
escaped = false;
continue;
}
if (ch === "\\") {
escaped = true;
continue;
}
if (ch === quote) {
quote = null;
}
continue;
}
if (ch === "'" || ch === '"' || ch === "`") {
quote = ch;
continue;
}
if (ch === "(") {
depth += 1;
continue;
}
if (ch === ")") {
depth -= 1;
if (depth === 0) {
return i;
}
}
}
return -1;
} }
function isOsTmpdirCall(node: ts.Expression): boolean { function splitTopLevelArguments(source: string): string[] {
return ( const out: string[] = [];
ts.isCallExpression(node) && let current = "";
node.arguments.length === 0 && let parenDepth = 0;
ts.isPropertyAccessExpression(node.expression) && let bracketDepth = 0;
node.expression.name.text === "tmpdir" && let braceDepth = 0;
isIdentifierNamed(node.expression.expression, "os") let quote: "'" | '"' | "`" | null = null;
); let escaped = false;
for (let i = 0; i < source.length; i += 1) {
const ch = source[i];
if (quote) {
current += ch;
if (escaped) {
escaped = false;
continue;
}
if (ch === "\\") {
escaped = true;
continue;
}
if (ch === quote) {
quote = null;
}
continue;
}
if (ch === "'" || ch === '"' || ch === "`") {
quote = ch;
current += ch;
continue;
}
if (ch === "(") {
parenDepth += 1;
current += ch;
continue;
}
if (ch === ")") {
if (parenDepth > 0) {
parenDepth -= 1;
}
current += ch;
continue;
}
if (ch === "[") {
bracketDepth += 1;
current += ch;
continue;
}
if (ch === "]") {
if (bracketDepth > 0) {
bracketDepth -= 1;
}
current += ch;
continue;
}
if (ch === "{") {
braceDepth += 1;
current += ch;
continue;
}
if (ch === "}") {
if (braceDepth > 0) {
braceDepth -= 1;
}
current += ch;
continue;
}
if (ch === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
out.push(current.trim());
current = "";
continue;
}
current += ch;
}
if (current.trim()) {
out.push(current.trim());
}
return out;
} }
function isDynamicTemplateSegment(node: ts.Expression): boolean { function isOsTmpdirExpression(argument: string): boolean {
return ts.isTemplateExpression(node); return /^os\s*\.\s*tmpdir\s*\(\s*\)$/u.test(argument.trim());
} }
function mightContainDynamicTmpdirJoin(source: string): boolean { function mightContainDynamicTmpdirJoin(source: string): boolean {
return source.includes("path.join") && source.includes("os.tmpdir") && source.includes("${"); return (
source.includes("path") &&
source.includes("join") &&
source.includes("tmpdir") &&
source.includes("${")
);
} }
function hasDynamicTmpdirJoin(source: string, filePath = "fixture.ts"): boolean { function hasDynamicTmpdirJoin(source: string): boolean {
if (!mightContainDynamicTmpdirJoin(source)) { if (!mightContainDynamicTmpdirJoin(source)) {
return false; return false;
} }
const sourceFile = ts.createSourceFile(
filePath,
source,
ts.ScriptTarget.Latest,
false,
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
let found = false;
const visit = (node: ts.Node): void => { const scanSource = stripCommentsForScan(source);
if (found) { const joinPattern = /path\s*\.\s*join\s*\(/gu;
return; let match: RegExpExecArray | null = joinPattern.exec(scanSource);
while (match) {
const openParenIndex = scanSource.indexOf("(", match.index);
if (openParenIndex !== -1) {
const closeParenIndex = findMatchingParen(scanSource, openParenIndex);
if (closeParenIndex !== -1) {
const argsSource = scanSource.slice(openParenIndex + 1, closeParenIndex);
const args = splitTopLevelArguments(argsSource);
if (args.length >= 2 && isOsTmpdirExpression(args[0])) {
for (const arg of args.slice(1)) {
const trimmed = arg.trim();
if (trimmed.startsWith("`") && trimmed.includes("${")) {
return true;
}
}
}
}
} }
if ( match = joinPattern.exec(scanSource);
ts.isCallExpression(node) && }
isPathJoinCall(node.expression) && return false;
node.arguments.length >= 2 &&
isOsTmpdirCall(node.arguments[0]) &&
node.arguments.slice(1).some((arg) => isDynamicTemplateSegment(arg))
) {
found = true;
return;
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return found;
} }
async function listTsFiles(dir: string): Promise<string[]> { async function listTsFiles(dir: string): Promise<string[]> {
@@ -135,13 +237,17 @@ function prefilterLikelyTmpdirJoinFiles(roots: readonly string[]): Set<string> |
"--glob", "--glob",
"!**/*.d.ts", "!**/*.d.ts",
"--glob", "--glob",
"!**/*.test-helpers.ts", "!**/*test-helpers*.ts",
"--glob", "--glob",
"!**/*.test-helpers.tsx", "!**/*test-helpers*.tsx",
"--glob", "--glob",
"!**/*.test-utils.ts", "!**/*test-utils*.ts",
"--glob", "--glob",
"!**/*.test-utils.tsx", "!**/*test-utils*.tsx",
"--glob",
"!**/*test-harness*.ts",
"--glob",
"!**/*test-harness*.tsx",
"--no-messages", "--no-messages",
]; ];
const strictDynamicCall = spawnSync( const strictDynamicCall = spawnSync(