mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
perf: speed type suppression inventory (#107874)
This commit is contained in:
@@ -33,7 +33,61 @@ export type TypeSuppressionReport = {
|
||||
};
|
||||
};
|
||||
|
||||
const TYPE_SUPPRESSION_CANDIDATE_PATTERN = /\bany\b|@ts-expect-error/u;
|
||||
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, {
|
||||
@@ -81,23 +135,30 @@ function addExpectErrorFindings(
|
||||
findings: TypeSuppressionFinding[],
|
||||
): void {
|
||||
const source = sourceFile.getFullText();
|
||||
const scanner = ts.createScanner(
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
file.endsWith(".tsx") ? ts.LanguageVariant.JSX : ts.LanguageVariant.Standard,
|
||||
source,
|
||||
);
|
||||
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
||||
if (
|
||||
token !== ts.SyntaxKind.SingleLineCommentTrivia &&
|
||||
token !== ts.SyntaxKind.MultiLineCommentTrivia
|
||||
) {
|
||||
continue;
|
||||
if (!source.includes("@ts-expect-error")) {
|
||||
return;
|
||||
}
|
||||
const comments = new Map<number, ts.CommentRange>();
|
||||
const addComments = (ranges: readonly ts.CommentRange[] | undefined): void => {
|
||||
for (const range of ranges ?? []) {
|
||||
comments.set(range.pos, range);
|
||||
}
|
||||
const comment = scanner.getTokenText();
|
||||
};
|
||||
const visit = (node: ts.Node): void => {
|
||||
addComments(ts.getLeadingCommentRanges(source, node.pos));
|
||||
addComments(ts.getTrailingCommentRanges(source, node.end));
|
||||
for (const child of node.getChildren(sourceFile)) {
|
||||
visit(child);
|
||||
}
|
||||
};
|
||||
visit(sourceFile);
|
||||
addComments(ts.getLeadingCommentRanges(source, sourceFile.endOfFileToken.pos));
|
||||
|
||||
for (const range of comments.values()) {
|
||||
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 = scanner.getTokenPos() + (match.index ?? 0);
|
||||
const position = range.pos + (match.index ?? 0);
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(position).line;
|
||||
findings.push({
|
||||
excerpt: match[0].trim(),
|
||||
@@ -127,9 +188,9 @@ export function collectTypeSuppressionReport(params: {
|
||||
continue;
|
||||
}
|
||||
const source = fs.readFileSync(absolutePath, "utf8");
|
||||
// Full AST parsing dominates the repository ratchet. Every reported construct
|
||||
// contains one of these literal markers, so marker-free files are safe to skip.
|
||||
if (!TYPE_SUPPRESSION_CANDIDATE_PATTERN.test(source)) {
|
||||
// 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 = ts.createSourceFile(file, source, ts.ScriptTarget.Latest);
|
||||
|
||||
@@ -30,8 +30,14 @@ describe("type suppression inventory", () => {
|
||||
const fixtureRoot = createFixture({
|
||||
"src/example.ts": `
|
||||
const prose = "as any and @ts-expect-error";
|
||||
const interpolated = \`value: \${value}\`;
|
||||
const first = value as any;
|
||||
const second = <any>value;
|
||||
const third = value as /* preserved trivia */ any;
|
||||
const fourth = </* preserved trivia */ any>value;
|
||||
const fifth = value as // preserved line trivia
|
||||
any;
|
||||
const typeOnly: any = value;
|
||||
// @ts-expect-error invalid contract fixture
|
||||
consume({ invalid: true });
|
||||
`,
|
||||
@@ -44,11 +50,11 @@ describe("type suppression inventory", () => {
|
||||
});
|
||||
|
||||
expect(report.summary).toMatchObject({
|
||||
findingCount: 3,
|
||||
findingCount: 6,
|
||||
kindCounts: {
|
||||
"as-any": 1,
|
||||
"as-any": 3,
|
||||
"expect-error": 1,
|
||||
"type-assertion-any": 1,
|
||||
"type-assertion-any": 2,
|
||||
},
|
||||
scannedFileCount: 2,
|
||||
touchedFileCount: 1,
|
||||
|
||||
Reference in New Issue
Block a user