mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-08 10:59:18 +00:00
* refactor: remove TypeScript 6 from plugin runtime and tooling Keep source plugin loading on Jiti and native module transforms, move development analysis and declaration builds to TypeScript 7, and preserve captured loader and input ownership contracts. Scaffolds compile to the same entry path without shipping a compiler in production. * refactor: use native checker for restart preflight detection * fix: keep test-directory docs out of native helper scans * fix: preserve native compiler tooling across CI runtimes Keep compiler-backed tests on the supported Node transport, preserve declared Windows aliases and partial-source analysis, and bind relocated SDK reports to their installed compiler. Retain strict package input admission and the existing fixture assertions. * test: compare messaging guard diagnostics without sorting * fix: bound native tooling memory and preserve bootstrap loading * fix: bound native declaration builds with tsdown scheduling * fix: retire compiler-only helpers from installed packages * test: migrate routing import scan to native parser * test(ui): wait for recovered model catalog receipt
226 lines
6.6 KiB
JavaScript
226 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Type Suppression Inventory reports unchecked any casts and expected TypeScript errors.
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import * as ts from "typescript/unstable/ast";
|
|
import {
|
|
REPO_SCAN_ROOTS,
|
|
REPO_SCAN_SKIPPED_DIR_NAMES,
|
|
listRepoFilesSync,
|
|
toPosixPath,
|
|
} from "./check-file-utils.js";
|
|
import { createNativeTypeScriptParser } from "./lib/native-typescript.mts";
|
|
import { collectTypeScriptCommentRanges } from "./lib/ts-guard-utils.mts";
|
|
|
|
type TypeSuppressionKind = "as-any" | "expect-error" | "type-assertion-any";
|
|
|
|
export type TypeSuppressionFinding = {
|
|
excerpt: string;
|
|
file: string;
|
|
kind: TypeSuppressionKind;
|
|
line: number;
|
|
};
|
|
|
|
export type TypeSuppressionReport = {
|
|
findings: TypeSuppressionFinding[];
|
|
scannedFileCount: number;
|
|
schemaVersion: 1;
|
|
summary: {
|
|
findingCount: number;
|
|
kindCounts: Record<TypeSuppressionKind, number>;
|
|
scannedFileCount: number;
|
|
touchedFileCount: number;
|
|
};
|
|
};
|
|
|
|
const TYPE_SUPPRESSION_WHITESPACE_PATTERN = /\s/u;
|
|
|
|
function skipTypeScriptTrivia(source: string, start: number): number {
|
|
let offset = start;
|
|
while (offset < source.length) {
|
|
const character = source[offset];
|
|
if (character && TYPE_SUPPRESSION_WHITESPACE_PATTERN.test(character)) {
|
|
offset += 1;
|
|
continue;
|
|
}
|
|
if (source.startsWith("/*", offset)) {
|
|
const commentEnd = source.indexOf("*/", offset + 2);
|
|
offset = commentEnd === -1 ? source.length : commentEnd + 2;
|
|
continue;
|
|
}
|
|
if (source.startsWith("//", offset)) {
|
|
offset += 2;
|
|
while (offset < source.length && !/[\r\n\u2028\u2029]/u.test(source[offset] ?? "")) {
|
|
offset += 1;
|
|
}
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
function isAnyKeywordAt(source: string, offset: number): boolean {
|
|
return (
|
|
source.startsWith("any", offset) && !/[A-Za-z0-9_$]/u.test(source[offset + "any".length] ?? "")
|
|
);
|
|
}
|
|
|
|
function hasTypeSuppressionTextCandidate(source: string): boolean {
|
|
if (source.includes("@ts-expect-error")) {
|
|
return true;
|
|
}
|
|
for (const match of source.matchAll(/\bas\b/gu)) {
|
|
if (
|
|
isAnyKeywordAt(source, skipTypeScriptTrivia(source, (match.index ?? 0) + match[0].length))
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
for (let offset = source.indexOf("<"); offset !== -1; offset = source.indexOf("<", offset + 1)) {
|
|
const anyOffset = skipTypeScriptTrivia(source, offset + 1);
|
|
if (
|
|
isAnyKeywordAt(source, anyOffset) &&
|
|
source[skipTypeScriptTrivia(source, anyOffset + "any".length)] === ">"
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function listCandidateFiles(repoRoot: string, roots: readonly string[]): string[] {
|
|
return listRepoFilesSync(repoRoot, {
|
|
includeFile: (file) => {
|
|
const pathSegments = toPosixPath(file).split("/");
|
|
return (
|
|
/\.[cm]?tsx?$/u.test(file) &&
|
|
!file.endsWith(".d.ts") &&
|
|
!pathSegments.some((segment) => REPO_SCAN_SKIPPED_DIR_NAMES.has(segment))
|
|
);
|
|
},
|
|
roots,
|
|
});
|
|
}
|
|
|
|
function addAnyCastFindings(
|
|
sourceFile: ts.SourceFile,
|
|
file: string,
|
|
findings: TypeSuppressionFinding[],
|
|
): void {
|
|
const visit = (node: ts.Node): void => {
|
|
const kind =
|
|
ts.isAsExpression(node) && node.type.kind === ts.SyntaxKind.AnyKeyword
|
|
? "as-any"
|
|
: ts.isTypeAssertion(node) && node.type.kind === ts.SyntaxKind.AnyKeyword
|
|
? "type-assertion-any"
|
|
: null;
|
|
if (kind) {
|
|
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
findings.push({
|
|
excerpt: node.getText(sourceFile).replace(/\s+/gu, " ").trim(),
|
|
file,
|
|
kind,
|
|
line: line + 1,
|
|
});
|
|
}
|
|
node.forEachChild(visit);
|
|
};
|
|
visit(sourceFile);
|
|
}
|
|
|
|
function addExpectErrorFindings(
|
|
sourceFile: ts.SourceFile,
|
|
file: string,
|
|
findings: TypeSuppressionFinding[],
|
|
): void {
|
|
const source = sourceFile.getFullText();
|
|
if (!source.includes("@ts-expect-error")) {
|
|
return;
|
|
}
|
|
for (const range of collectTypeScriptCommentRanges(sourceFile)) {
|
|
const comment = source.slice(range.pos, range.end);
|
|
const markerPattern = /@ts-expect-error[^\r\n]*/gu;
|
|
for (const match of comment.matchAll(markerPattern)) {
|
|
const position = range.pos + (match.index ?? 0);
|
|
const line = sourceFile.getLineAndCharacterOfPosition(position).line;
|
|
findings.push({
|
|
excerpt: match[0].trim(),
|
|
file,
|
|
kind: "expect-error",
|
|
line: line + 1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export function collectTypeSuppressionReport(params: {
|
|
files?: readonly string[];
|
|
repoRoot: string;
|
|
roots?: readonly string[];
|
|
}): TypeSuppressionReport {
|
|
const files = [
|
|
...(params.files ?? listCandidateFiles(params.repoRoot, params.roots ?? REPO_SCAN_ROOTS)),
|
|
]
|
|
.map(toPosixPath)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
const findings: TypeSuppressionFinding[] = [];
|
|
const parser = createNativeTypeScriptParser({ cwd: params.repoRoot });
|
|
try {
|
|
for (const file of files) {
|
|
const absolutePath = path.join(params.repoRoot, file);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
continue;
|
|
}
|
|
const source = fs.readFileSync(absolutePath, "utf8");
|
|
// Full AST parsing dominates the repository ratchet. This syntax-shaped text
|
|
// gate keeps false positives cheap; the AST remains the source of truth.
|
|
if (!hasTypeSuppressionTextCandidate(source)) {
|
|
continue;
|
|
}
|
|
const sourceFile = parser.parseSourceFile(file, source);
|
|
addAnyCastFindings(sourceFile, file, findings);
|
|
addExpectErrorFindings(sourceFile, file, findings);
|
|
}
|
|
} finally {
|
|
parser.close();
|
|
}
|
|
|
|
findings.sort(
|
|
(left, right) =>
|
|
left.file.localeCompare(right.file) ||
|
|
left.line - right.line ||
|
|
left.kind.localeCompare(right.kind),
|
|
);
|
|
const kindCounts: Record<TypeSuppressionKind, number> = {
|
|
"as-any": 0,
|
|
"expect-error": 0,
|
|
"type-assertion-any": 0,
|
|
};
|
|
for (const finding of findings) {
|
|
kindCounts[finding.kind] = kindCounts[finding.kind] + 1;
|
|
}
|
|
|
|
return {
|
|
findings,
|
|
scannedFileCount: files.length,
|
|
schemaVersion: 1,
|
|
summary: {
|
|
findingCount: findings.length,
|
|
kindCounts,
|
|
scannedFileCount: files.length,
|
|
touchedFileCount: new Set(findings.map((finding) => finding.file)).size,
|
|
},
|
|
};
|
|
}
|
|
|
|
function main(): void {
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
process.stdout.write(`${JSON.stringify(collectTypeSuppressionReport({ repoRoot }), null, 2)}\n`);
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
main();
|
|
}
|