refactor(scripts): share repository file discovery

This commit is contained in:
Peter Steinberger
2026-07-13 21:35:24 -07:00
parent d45dd28df6
commit 96c0e89291
4 changed files with 87 additions and 164 deletions
+61
View File
@@ -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<string> = 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>;
},
): 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;
+8 -56
View File
@@ -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 = "<dynamic>";
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[] = [];
+4 -54
View File
@@ -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<SkipInventoryReason, number> = {
"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 {
+14 -54
View File
@@ -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);
}