From 96c0e89291bb556286cc75e9944d7293f1346e4c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 05:24:19 +0100 Subject: [PATCH] refactor(scripts): share repository file discovery --- scripts/check-file-utils.ts | 61 ++++++++++++++++++++++++ scripts/test-env-mutation-report.ts | 64 ++++--------------------- scripts/test-skip-inventory.ts | 58 ++--------------------- scripts/type-suppression-inventory.ts | 68 ++++++--------------------- 4 files changed, 87 insertions(+), 164 deletions(-) diff --git a/scripts/check-file-utils.ts b/scripts/check-file-utils.ts index 003bf29ef0a..07e6b1c957c 100644 --- a/scripts/check-file-utils.ts +++ b/scripts/check-file-utils.ts @@ -1,8 +1,19 @@ // Check File Utils helper supports OpenClaw script workflows. +import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; const DEFAULT_SKIPPED_DIR_NAMES = new Set(["node_modules", "dist", "coverage", ".generated"]); +export const REPO_SCAN_ROOTS = ["src", "test", "extensions", "packages", "ui", "scripts"] as const; +export const REPO_SCAN_SKIPPED_DIR_NAMES: ReadonlySet = new Set([ + ".artifacts", + ".generated", + "coverage", + "dist", + "fixtures", + "node_modules", + "vendor", +]); export function isCodeFile(filePath: string): boolean { if (filePath.endsWith(".d.ts")) { @@ -11,6 +22,20 @@ export function isCodeFile(filePath: string): boolean { return /\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(filePath); } +export function isTestRelatedFile(relativePath: string): boolean { + return ( + /(?:^|[/.])(?:test|spec)\.[cm]?[jt]sx?$/u.test(relativePath) || + /\.(?:e2e|live)\.test\.[cm]?[jt]sx?$/u.test(relativePath) || + /\.(?:test-helpers|test-utils|test-harness|test-support)\.[cm]?[jt]sx?$/u.test(relativePath) || + /-(?:test-helpers|test-utils|test-harness|test-support)\.[cm]?[jt]sx?$/u.test(relativePath) || + /(?:^|\/)(?:test|tests|test-helpers|test-utils|test-harness|test-support)\//u.test( + relativePath, + ) || + relativePath.startsWith("scripts/e2e/") || + /^scripts\/.*-(?:client|e2e|harness|probe|smoke)\.[cm]?[jt]s$/u.test(relativePath) + ); +} + export function collectFilesSync( rootDir: string, options: { @@ -51,6 +76,42 @@ export function collectFilesSync( return files; } +export function listRepoFilesSync( + repoRoot: string, + options: { + includeFile: (relativePath: string) => boolean; + roots?: readonly string[]; + skipDirNames?: ReadonlySet; + }, +): string[] { + const roots = options.roots ?? REPO_SCAN_ROOTS; + try { + return execFileSync("git", ["-C", repoRoot, "ls-files", "--", ...roots], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }) + .split(/\r?\n/u) + .filter(Boolean) + .map(toPosixPath) + .filter(options.includeFile) + .toSorted((left, right) => left.localeCompare(right)); + } catch { + return roots + .flatMap((root) => { + const absoluteRoot = path.join(repoRoot, root); + if (!fs.existsSync(absoluteRoot)) { + return []; + } + return collectFilesSync(absoluteRoot, { + includeFile: (filePath) => + options.includeFile(toPosixPath(path.relative(repoRoot, filePath))), + skipDirNames: options.skipDirNames ?? REPO_SCAN_SKIPPED_DIR_NAMES, + }).map((filePath) => toPosixPath(path.relative(repoRoot, filePath))); + }) + .toSorted((left, right) => left.localeCompare(right)); + } +} + export function toPosixPath(filePath: string): string { if (path.sep === "/") { return filePath; diff --git a/scripts/test-env-mutation-report.ts b/scripts/test-env-mutation-report.ts index def09d3b64f..fe2b0dcce16 100644 --- a/scripts/test-env-mutation-report.ts +++ b/scripts/test-env-mutation-report.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node // Test Env Mutation Report script supports OpenClaw repository automation. -import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; -import { collectFilesSync, isCodeFile, toPosixPath } from "./check-file-utils.js"; +import { isCodeFile, isTestRelatedFile, listRepoFilesSync } from "./check-file-utils.js"; type EnvMutationOperation = "assign" | "delete" | "replace" | "stubEnv"; @@ -37,16 +36,6 @@ export type TestEnvMutationReport = { }; const DYNAMIC_ENV_KEY = ""; -const DEFAULT_SCAN_ROOTS = ["src", "test", "extensions", "packages", "ui", "scripts"]; -const DEFAULT_SKIPPED_DIR_NAMES = new Set([ - ".artifacts", - ".generated", - "coverage", - "dist", - "fixtures", - "node_modules", - "vendor", -]); const TRACKED_ENV_KEYS = new Set([ "HOME", "HOMEDRIVE", @@ -75,49 +64,10 @@ const DEFAULT_ALLOWED_FILES = new Map([ ], ]); -function isTestRelatedFile(relativePath: string): boolean { - return ( - /(?:^|[/.])(?:test|spec)\.[cm]?[jt]sx?$/u.test(relativePath) || - /\.(?:e2e|live)\.test\.[cm]?[jt]sx?$/u.test(relativePath) || - /\.(?:test-helpers|test-utils|test-harness|test-support)\.[cm]?[jt]sx?$/u.test(relativePath) || - /-(?:test-helpers|test-utils|test-harness|test-support)\.[cm]?[jt]sx?$/u.test(relativePath) || - /(?:^|\/)(?:test|tests|test-helpers|test-utils|test-harness|test-support)\//u.test( - relativePath, - ) || - relativePath.startsWith("scripts/e2e/") || - /^scripts\/.*-(?:client|e2e|harness|probe|smoke)\.[cm]?[jt]s$/u.test(relativePath) - ); -} - -function listGitFiles(repoRoot: string): string[] | null { - try { - const stdout = execFileSync("git", ["-C", repoRoot, "ls-files", "--", ...DEFAULT_SCAN_ROOTS], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - return stdout.split(/\r?\n/u).filter(Boolean); - } catch { - return null; - } -} - function listCandidateFiles(repoRoot: string): string[] { - const gitFiles = listGitFiles(repoRoot); - const relativeFiles = - gitFiles ?? - DEFAULT_SCAN_ROOTS.flatMap((root) => { - const absoluteRoot = path.join(repoRoot, root); - if (!fs.existsSync(absoluteRoot)) { - return []; - } - return collectFilesSync(absoluteRoot, { - includeFile: isCodeFile, - skipDirNames: DEFAULT_SKIPPED_DIR_NAMES, - }).map((filePath) => toPosixPath(path.relative(repoRoot, filePath))); - }); - return relativeFiles - .filter((file) => isCodeFile(file) && isTestRelatedFile(file)) - .toSorted((left, right) => left.localeCompare(right)); + return listRepoFilesSync(repoRoot, { + includeFile: (file) => isCodeFile(file) && isTestRelatedFile(file), + }); } function isIdentifier(node: ts.Node, text: string): boolean { @@ -193,7 +143,9 @@ function createFinding(params: { operation: EnvMutationOperation; sourceFile: ts.SourceFile; }): TestEnvMutationFinding { - const { line } = params.sourceFile.getLineAndCharacterOfPosition(params.node.getStart()); + const { line } = params.sourceFile.getLineAndCharacterOfPosition( + params.node.getStart(params.sourceFile), + ); const allowReason = params.allowedFiles.get(params.file); return { allowed: allowReason !== undefined, @@ -213,7 +165,7 @@ function scanFile(params: { }): TestEnvMutationFinding[] { const absolutePath = path.join(params.repoRoot, params.file); const source = fs.readFileSync(absolutePath, "utf8"); - const sourceFile = ts.createSourceFile(params.file, source, ts.ScriptTarget.Latest, true); + const sourceFile = ts.createSourceFile(params.file, source, ts.ScriptTarget.Latest); const lines = source.split(/\r?\n/u); const findings: TestEnvMutationFinding[] = []; diff --git a/scripts/test-skip-inventory.ts b/scripts/test-skip-inventory.ts index 4506c26b889..e6d9563acf2 100644 --- a/scripts/test-skip-inventory.ts +++ b/scripts/test-skip-inventory.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node // Test Skip Inventory reports skipped, conditional, todo, and focused tests. -import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; -import { collectFilesSync, isCodeFile, toPosixPath } from "./check-file-utils.js"; +import { isCodeFile, isTestRelatedFile, listRepoFilesSync } from "./check-file-utils.js"; type SkipInventoryKind = "alias" | "call"; type SkipInventoryReason = @@ -42,16 +41,6 @@ export type TestSkipInventoryReport = { }; }; -const DEFAULT_SCAN_ROOTS = ["src", "test", "extensions", "packages", "ui", "scripts"]; -const DEFAULT_SKIPPED_DIR_NAMES = new Set([ - ".artifacts", - ".generated", - "coverage", - "dist", - "fixtures", - "node_modules", - "vendor", -]); const EMPTY_REASON_COUNTS: Record = { "conditional-skip": 0, "explicit-skip": 0, @@ -65,49 +54,10 @@ const SKIP_METHODS = new Set(["only", "runIf", "skip", "skipIf", "todo"]); const TEST_TARGETS = new Set(["describe", "it", "test"]); const TRANSPARENT_CHAIN_METHODS = new Set(["concurrent", "each", "sequential"]); -function isTestRelatedFile(relativePath: string): boolean { - return ( - /(?:^|[/.])(?:test|spec)\.[cm]?[jt]sx?$/u.test(relativePath) || - /\.(?:e2e|live)\.test\.[cm]?[jt]sx?$/u.test(relativePath) || - /\.(?:test-helpers|test-utils|test-harness|test-support)\.[cm]?[jt]sx?$/u.test(relativePath) || - /-(?:test-helpers|test-utils|test-harness|test-support)\.[cm]?[jt]sx?$/u.test(relativePath) || - /(?:^|\/)(?:test|tests|test-helpers|test-utils|test-harness|test-support)\//u.test( - relativePath, - ) || - relativePath.startsWith("scripts/e2e/") || - /^scripts\/.*-(?:client|e2e|harness|probe|smoke)\.[cm]?[jt]s$/u.test(relativePath) - ); -} - -function listGitFiles(repoRoot: string): string[] | null { - try { - const stdout = execFileSync("git", ["-C", repoRoot, "ls-files", "--", ...DEFAULT_SCAN_ROOTS], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - return stdout.split(/\r?\n/u).filter(Boolean); - } catch { - return null; - } -} - function listCandidateFiles(repoRoot: string): string[] { - const gitFiles = listGitFiles(repoRoot); - const relativeFiles = - gitFiles ?? - DEFAULT_SCAN_ROOTS.flatMap((root) => { - const absoluteRoot = path.join(repoRoot, root); - if (!fs.existsSync(absoluteRoot)) { - return []; - } - return collectFilesSync(absoluteRoot, { - includeFile: isCodeFile, - skipDirNames: DEFAULT_SKIPPED_DIR_NAMES, - }).map((filePath) => toPosixPath(path.relative(repoRoot, filePath))); - }); - return relativeFiles - .filter((file) => isCodeFile(file) && isTestRelatedFile(file)) - .toSorted((left, right) => left.localeCompare(right)); + return listRepoFilesSync(repoRoot, { + includeFile: (file) => isCodeFile(file) && isTestRelatedFile(file), + }); } function expressionText(sourceFile: ts.SourceFile, node: ts.Node): string { diff --git a/scripts/type-suppression-inventory.ts b/scripts/type-suppression-inventory.ts index 59bf70db98d..71fee1ebc68 100644 --- a/scripts/type-suppression-inventory.ts +++ b/scripts/type-suppression-inventory.ts @@ -1,12 +1,16 @@ #!/usr/bin/env node // Type Suppression Inventory reports unchecked any casts and expected TypeScript errors. -import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; -import { collectFilesSync, isCodeFile, toPosixPath } from "./check-file-utils.js"; +import { + REPO_SCAN_ROOTS, + REPO_SCAN_SKIPPED_DIR_NAMES, + listRepoFilesSync, + toPosixPath, +} from "./check-file-utils.js"; type TypeSuppressionKind = "as-any" | "expect-error" | "type-assertion-any"; @@ -29,58 +33,20 @@ export type TypeSuppressionReport = { }; }; -const DEFAULT_SCAN_ROOTS = ["src", "test", "extensions", "packages", "ui", "scripts"]; const TYPE_SUPPRESSION_CANDIDATE_PATTERN = /\bany\b|@ts-expect-error/u; -const DEFAULT_SKIPPED_DIR_NAMES = new Set([ - ".artifacts", - ".generated", - "coverage", - "dist", - "fixtures", - "node_modules", - "vendor", -]); - -function listGitFiles(repoRoot: string, roots: readonly string[]): string[] | null { - try { - const stdout = execFileSync("git", ["-C", repoRoot, "ls-files", "--", ...roots], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - return stdout.split(/\r?\n/u).filter(Boolean); - } catch { - return null; - } -} function listCandidateFiles(repoRoot: string, roots: readonly string[]): string[] { - const gitFiles = listGitFiles(repoRoot, roots); - const relativeFiles = - gitFiles ?? - roots.flatMap((root) => { - const absoluteRoot = path.join(repoRoot, root); - if (!fs.existsSync(absoluteRoot)) { - return []; - } - return collectFilesSync(absoluteRoot, { - includeFile: isCodeFile, - skipDirNames: DEFAULT_SKIPPED_DIR_NAMES, - }).map((filePath) => toPosixPath(path.relative(repoRoot, filePath))); - }); - return relativeFiles - .filter((file) => { + return listRepoFilesSync(repoRoot, { + includeFile: (file) => { const pathSegments = toPosixPath(file).split("/"); return ( /\.[cm]?tsx?$/u.test(file) && !file.endsWith(".d.ts") && - !pathSegments.some((segment) => DEFAULT_SKIPPED_DIR_NAMES.has(segment)) + !pathSegments.some((segment) => REPO_SCAN_SKIPPED_DIR_NAMES.has(segment)) ); - }) - .toSorted((left, right) => left.localeCompare(right)); -} - -function sourceKindForFile(file: string): ts.ScriptKind { - return file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS; + }, + roots, + }); } function addAnyCastFindings( @@ -149,7 +115,7 @@ export function collectTypeSuppressionReport(params: { roots?: readonly string[]; }): TypeSuppressionReport { const files = [ - ...(params.files ?? listCandidateFiles(params.repoRoot, params.roots ?? DEFAULT_SCAN_ROOTS)), + ...(params.files ?? listCandidateFiles(params.repoRoot, params.roots ?? REPO_SCAN_ROOTS)), ] .map(toPosixPath) .toSorted((left, right) => left.localeCompare(right)); @@ -166,13 +132,7 @@ export function collectTypeSuppressionReport(params: { if (!TYPE_SUPPRESSION_CANDIDATE_PATTERN.test(source)) { continue; } - const sourceFile = ts.createSourceFile( - file, - source, - ts.ScriptTarget.Latest, - false, - sourceKindForFile(file), - ); + const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest); addAnyCastFindings(sourceFile, file, findings); addExpectErrorFindings(sourceFile, file, findings); }