mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
refactor(scripts): use native TypeScript AST helpers
This commit is contained in:
+11
-56
@@ -9,8 +9,10 @@ import {
|
||||
BUNDLED_PLUGIN_PATH_PREFIX,
|
||||
BUNDLED_PLUGIN_ROOT_DIR,
|
||||
} from "./lib/bundled-plugin-paths.mjs";
|
||||
import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs";
|
||||
import { optionalBundledClusterSet } from "./lib/optional-bundled-clusters.mjs";
|
||||
import { escapeRegExp } from "./lib/regexp.mjs";
|
||||
import { toLine } from "./lib/ts-guard-utils.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const srcRoot = path.join(repoRoot, "src");
|
||||
@@ -157,10 +159,6 @@ async function walkAllCodeFiles(rootDir, options = {}) {
|
||||
return out.toSorted((left, right) => normalizePath(left).localeCompare(normalizePath(right)));
|
||||
}
|
||||
|
||||
function toLine(sourceFile, node) {
|
||||
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
||||
}
|
||||
|
||||
function resolveRelativeSpecifier(specifier, importerFile) {
|
||||
if (!specifier.startsWith(".")) {
|
||||
return null;
|
||||
@@ -213,27 +211,9 @@ function collectPluginSdkImports(filePath, sourceFile) {
|
||||
});
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
push("import", node.moduleSpecifier, node.moduleSpecifier.text);
|
||||
} else if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
node.moduleSpecifier &&
|
||||
ts.isStringLiteral(node.moduleSpecifier)
|
||||
) {
|
||||
push("export", node.moduleSpecifier, node.moduleSpecifier.text);
|
||||
} else if (
|
||||
ts.isCallExpression(node) &&
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
node.arguments.length === 1 &&
|
||||
ts.isStringLiteral(node.arguments[0])
|
||||
) {
|
||||
push("dynamic-import", node.arguments[0], node.arguments[0].text);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => {
|
||||
push(kind, specifierNode, specifier);
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -245,15 +225,7 @@ async function collectCorePluginSdkImports() {
|
||||
continue;
|
||||
}
|
||||
const source = await fs.readFile(filePath, "utf8");
|
||||
const scriptKind =
|
||||
filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
);
|
||||
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
||||
inventory.push(...collectPluginSdkImports(filePath, sourceFile));
|
||||
}
|
||||
return inventory.toSorted(compareImports);
|
||||
@@ -284,20 +256,11 @@ function collectOptionalClusterStaticImports(filePath, sourceFile) {
|
||||
});
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
push("import", node.moduleSpecifier, node.moduleSpecifier.text);
|
||||
} else if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
node.moduleSpecifier &&
|
||||
ts.isStringLiteral(node.moduleSpecifier)
|
||||
) {
|
||||
push("export", node.moduleSpecifier, node.moduleSpecifier.text);
|
||||
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => {
|
||||
if (kind !== "dynamic-import") {
|
||||
push(kind, specifierNode, specifier);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -310,15 +273,7 @@ async function collectOptionalClusterStaticLeaks() {
|
||||
continue;
|
||||
}
|
||||
const source = await fs.readFile(filePath, "utf8");
|
||||
const scriptKind =
|
||||
filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
);
|
||||
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
||||
inventory.push(...collectOptionalClusterStaticImports(filePath, sourceFile));
|
||||
}
|
||||
return inventory.toSorted((left, right) => {
|
||||
|
||||
@@ -14,64 +14,44 @@ import {
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
const defaultRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
||||
|
||||
function readStringLiteral(node) {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
return node.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isTypeOnlyImportDeclaration(node) {
|
||||
const clause = node.importClause;
|
||||
if (!clause) {
|
||||
return false;
|
||||
}
|
||||
if (clause.isTypeOnly) {
|
||||
return true;
|
||||
}
|
||||
if (clause.name) {
|
||||
return false;
|
||||
}
|
||||
const bindings = clause.namedBindings;
|
||||
return (
|
||||
Boolean(bindings) &&
|
||||
ts.isNamedImports(bindings) &&
|
||||
bindings.elements.length > 0 &&
|
||||
bindings.elements.every((element) => element.isTypeOnly)
|
||||
return Boolean(
|
||||
clause &&
|
||||
(ts.isTypeOnlyImportDeclaration(clause) ||
|
||||
(!clause.name &&
|
||||
ts.isNamedImports(clause.namedBindings) &&
|
||||
clause.namedBindings.elements.length > 0 &&
|
||||
clause.namedBindings.elements.every(ts.isTypeOnlyImportOrExportDeclaration))),
|
||||
);
|
||||
}
|
||||
|
||||
function isTypeOnlyExportDeclaration(node) {
|
||||
if (node.isTypeOnly === true) {
|
||||
return true;
|
||||
}
|
||||
const clause = node.exportClause;
|
||||
return (
|
||||
Boolean(clause) &&
|
||||
ts.isNamedExports(clause) &&
|
||||
clause.elements.length > 0 &&
|
||||
clause.elements.every((element) => element.isTypeOnly)
|
||||
node.isTypeOnly === true ||
|
||||
Boolean(
|
||||
clause &&
|
||||
ts.isNamedExports(clause) &&
|
||||
clause.elements.length > 0 &&
|
||||
clause.elements.every(ts.isTypeOnlyImportOrExportDeclaration),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function readDeclarationName(node) {
|
||||
function isExecuteDeclaration(node) {
|
||||
if (
|
||||
(ts.isFunctionDeclaration(node) ||
|
||||
ts.isMethodDeclaration(node) ||
|
||||
ts.isVariableDeclaration(node)) &&
|
||||
node.name &&
|
||||
ts.isIdentifier(node.name)
|
||||
!ts.isFunctionDeclaration(node) &&
|
||||
!ts.isMethodDeclaration(node) &&
|
||||
!ts.isVariableDeclaration(node) &&
|
||||
!ts.isPropertyAssignment(node)
|
||||
) {
|
||||
return node.name.text;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ts.isPropertyAssignment(node)) {
|
||||
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) {
|
||||
return node.name.text;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
const name = ts.getNameOfDeclaration(node);
|
||||
return Boolean(
|
||||
name && (ts.isIdentifier(name) || ts.isStringLiteral(name)) && name.text === "execute",
|
||||
);
|
||||
}
|
||||
|
||||
function isIgnoredTestHelperContent(content) {
|
||||
@@ -99,7 +79,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
const staticRuntimeImports = new Map();
|
||||
const dynamicImports = new Map();
|
||||
const directExecuteImports = [];
|
||||
const declarationStack = [];
|
||||
|
||||
const addLine = (map, specifier, line) => {
|
||||
const lines = map.get(specifier) ?? [];
|
||||
@@ -108,11 +87,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
};
|
||||
|
||||
const visit = (node) => {
|
||||
const declarationName = readDeclarationName(node);
|
||||
if (declarationName) {
|
||||
declarationStack.push(declarationName);
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isImportDeclaration(node) &&
|
||||
ts.isStringLiteral(node.moduleSpecifier) &&
|
||||
@@ -135,11 +109,11 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
const specifier = readStringLiteral(node.arguments[0]);
|
||||
const specifier = ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null;
|
||||
if (specifier) {
|
||||
const line = toLine(sourceFile, node);
|
||||
addLine(dynamicImports, specifier, line);
|
||||
if (declarationStack.includes("execute")) {
|
||||
if (ts.findAncestor(node, isExecuteDeclaration)) {
|
||||
directExecuteImports.push({
|
||||
line,
|
||||
reason: `direct dynamic import of "${specifier}" inside execute path; move it behind a cached loader`,
|
||||
@@ -149,9 +123,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
if (declarationName) {
|
||||
declarationStack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
|
||||
@@ -16,19 +16,9 @@ const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
const uiSourceDir = path.join(repoRoot, "ui", "src", "ui");
|
||||
const allowedCallsites = new Set([path.join(uiSourceDir, "open-external-url.ts")]);
|
||||
|
||||
function asPropertyAccess(expression) {
|
||||
if (ts.isPropertyAccessExpression(expression)) {
|
||||
return expression;
|
||||
}
|
||||
if (typeof ts.isPropertyAccessChain === "function" && ts.isPropertyAccessChain(expression)) {
|
||||
return expression;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isRawWindowOpenCall(expression) {
|
||||
const propertyAccess = asPropertyAccess(unwrapExpression(expression));
|
||||
if (!propertyAccess || propertyAccess.name.text !== "open") {
|
||||
const propertyAccess = unwrapExpression(expression);
|
||||
if (!ts.isPropertyAccessExpression(propertyAccess) || propertyAccess.name.text !== "open") {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,13 +71,7 @@ async function collectViolations() {
|
||||
|
||||
for (const filePath of files) {
|
||||
const sourceText = readFileSync(filePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
|
||||
|
||||
function push(kind, specifierNode, specifier) {
|
||||
const subpath = parsePluginSdkSubpath(specifier);
|
||||
|
||||
@@ -49,16 +49,6 @@ function normalizeRawCopyText(raw: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function lineNumberForOffset(source: string, offset: number): number {
|
||||
let line = 1;
|
||||
for (let index = 0; index < offset && index < source.length; index += 1) {
|
||||
if (source.charCodeAt(index) === 10) {
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function parseDoubleQuotedString(raw: string): string {
|
||||
try {
|
||||
return JSON.parse(`"${raw}"`) as string;
|
||||
@@ -118,6 +108,7 @@ export function collectControlUiRawCopyFromSource(params: {
|
||||
const { filePath, source, sourceFile } = params;
|
||||
const repoPath = toRepoPath(filePath);
|
||||
const findings: RawCopyFinding[] = [];
|
||||
const toLine = (offset: number) => sourceFile.getLineAndCharacterOfPosition(offset).line + 1;
|
||||
const staticAttrPattern =
|
||||
/\b(aria-label|placeholder|title)\s*=\s*"((?:(?!\$\{)[^"\\]|\\.)*?\p{L}(?:(?!\$\{)[^"\\]|\\.)*?)"/gu;
|
||||
for (const match of source.matchAll(staticAttrPattern)) {
|
||||
@@ -125,7 +116,7 @@ export function collectControlUiRawCopyFromSource(params: {
|
||||
if (rawText) {
|
||||
pushRawCopyFinding(findings, {
|
||||
kind: "html-attribute",
|
||||
line: lineNumberForOffset(source, match.index ?? 0),
|
||||
line: toLine(match.index ?? 0),
|
||||
name: match[1] ?? "attribute",
|
||||
path: repoPath,
|
||||
text: parseDoubleQuotedString(rawText),
|
||||
@@ -140,7 +131,7 @@ export function collectControlUiRawCopyFromSource(params: {
|
||||
if (rawText) {
|
||||
pushRawCopyFinding(findings, {
|
||||
kind: "object-property",
|
||||
line: lineNumberForOffset(source, match.index ?? 0),
|
||||
line: toLine(match.index ?? 0),
|
||||
name: match[1] ?? "property",
|
||||
path: repoPath,
|
||||
text: parseDoubleQuotedString(rawText),
|
||||
@@ -162,7 +153,7 @@ export function collectControlUiRawCopyFromSource(params: {
|
||||
...node.template.templateSpans.map((span) => span.literal.text),
|
||||
].join(INTERPOLATION_MARKER);
|
||||
}
|
||||
const line = lineNumberForOffset(source, node.template.getStart(sourceFile));
|
||||
const line = toLine(node.template.getStart(sourceFile));
|
||||
for (const match of logicalText.matchAll(attrPattern)) {
|
||||
const rawText = match[2];
|
||||
if (rawText?.includes(INTERPOLATION_MARKER)) {
|
||||
@@ -199,13 +190,7 @@ async function collectFindings(): Promise<RawCopyFinding[]> {
|
||||
const findings: RawCopyFinding[] = [];
|
||||
for (const filePath of files.toSorted((left, right) => left.localeCompare(right))) {
|
||||
const source = await readFile(filePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
||||
findings.push(...collectControlUiRawCopyFromSource({ filePath, source, sourceFile }));
|
||||
}
|
||||
return findings;
|
||||
|
||||
@@ -239,10 +239,9 @@ export function formatGroupedInventoryHuman(params, inventory) {
|
||||
/** Parse TypeScript files and collect sorted inventory entries from each source file. */
|
||||
export async function collectTypeScriptInventory(params) {
|
||||
const inventory = [];
|
||||
const scriptKind = params.scriptKind ?? params.ts.ScriptKind.TS;
|
||||
|
||||
for (const filePath of params.files) {
|
||||
const cacheKey = `${scriptKind}:${filePath}`;
|
||||
const cacheKey = filePath;
|
||||
let sourceFile = parsedTypeScriptSourceCache.get(cacheKey);
|
||||
if (!sourceFile) {
|
||||
let source = sourceTextCache.get(filePath);
|
||||
@@ -258,7 +257,6 @@ export async function collectTypeScriptInventory(params) {
|
||||
source,
|
||||
params.ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
);
|
||||
parsedTypeScriptSourceCache.set(cacheKey, sourceFile);
|
||||
}
|
||||
|
||||
@@ -200,27 +200,8 @@ function isTempDirHelperImportSpec(filePath, specifier) {
|
||||
return stripKnownExtension(resolvedPath) === stripKnownExtension(TEMP_DIR_HELPER_PATH);
|
||||
}
|
||||
|
||||
function scriptKindForFile(filePath) {
|
||||
if (/\.[cm]?tsx$/u.test(filePath)) {
|
||||
return ts.ScriptKind.TSX;
|
||||
}
|
||||
if (/\.[cm]?jsx$/u.test(filePath)) {
|
||||
return ts.ScriptKind.JSX;
|
||||
}
|
||||
if (/\.[cm]?js$/u.test(filePath)) {
|
||||
return ts.ScriptKind.JS;
|
||||
}
|
||||
return ts.ScriptKind.TS;
|
||||
}
|
||||
|
||||
function createSourceFile(filePath, sourceText) {
|
||||
return ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKindForFile(filePath),
|
||||
);
|
||||
return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
|
||||
}
|
||||
|
||||
function lineForNode(sourceFile, node) {
|
||||
|
||||
Reference in New Issue
Block a user