mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
test: speed up import-heavy tests
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { BUNDLED_PLUGIN_PATH_PREFIX } from "./lib/bundled-plugin-paths.mjs";
|
||||
import {
|
||||
collectTypeScriptInventory,
|
||||
collectModuleReferencesFromSource,
|
||||
normalizeRepoPath,
|
||||
resolveRepoSpecifier,
|
||||
visitModuleSpecifiers,
|
||||
writeLine,
|
||||
} from "./lib/guard-inventory-utils.mjs";
|
||||
import {
|
||||
collectTypeScriptFilesFromRoots,
|
||||
resolveSourceRoots,
|
||||
runAsScript,
|
||||
toLine,
|
||||
} from "./lib/ts-guard-utils.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
@@ -37,7 +35,7 @@ function pushEntry(entries, entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
function scanPluginSdkExtensionFacadeSmells(sourceFile, filePath) {
|
||||
function scanPluginSdkExtensionFacadeSmells(source, filePath) {
|
||||
const relativeFile = normalizeRepoPath(repoRoot, filePath);
|
||||
if (!relativeFile.startsWith("src/plugin-sdk/")) {
|
||||
return [];
|
||||
@@ -45,28 +43,28 @@ function scanPluginSdkExtensionFacadeSmells(sourceFile, filePath) {
|
||||
|
||||
const entries = [];
|
||||
|
||||
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifier, specifierNode }) => {
|
||||
for (const { kind, line, specifier } of collectModuleReferencesFromSource(source)) {
|
||||
if (kind !== "export") {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
|
||||
if (!resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
pushEntry(entries, {
|
||||
category: "plugin-sdk-extension-facade",
|
||||
file: relativeFile,
|
||||
line: toLine(sourceFile, specifierNode),
|
||||
line,
|
||||
kind,
|
||||
specifier,
|
||||
resolvedPath,
|
||||
reason: "plugin-sdk public surface re-exports extension-owned implementation",
|
||||
});
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function scanRuntimeTypeImplementationSmells(sourceFile, filePath) {
|
||||
function scanRuntimeTypeImplementationSmells(source, filePath) {
|
||||
const relativeFile = normalizeRepoPath(repoRoot, filePath);
|
||||
if (!/^src\/plugins\/runtime\/types(?:-[^/]+)?\.ts$/.test(relativeFile)) {
|
||||
return [];
|
||||
@@ -74,39 +72,32 @@ function scanRuntimeTypeImplementationSmells(sourceFile, filePath) {
|
||||
|
||||
const entries = [];
|
||||
|
||||
function visit(node) {
|
||||
if (
|
||||
ts.isImportTypeNode(node) &&
|
||||
ts.isLiteralTypeNode(node.argument) &&
|
||||
ts.isStringLiteral(node.argument.literal)
|
||||
) {
|
||||
const specifier = node.argument.literal.text;
|
||||
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
|
||||
if (
|
||||
resolvedPath &&
|
||||
(/^src\/plugins\/runtime\/runtime-[^/]+\.ts$/.test(resolvedPath) ||
|
||||
/^extensions\/[^/]+\/runtime-api\.[^/]+$/.test(resolvedPath))
|
||||
) {
|
||||
pushEntry(entries, {
|
||||
category: "runtime-type-implementation-edge",
|
||||
file: relativeFile,
|
||||
line: toLine(sourceFile, node.argument.literal),
|
||||
kind: "import-type",
|
||||
specifier,
|
||||
resolvedPath,
|
||||
reason: "runtime type file references implementation shim directly",
|
||||
});
|
||||
}
|
||||
for (const { kind, line, specifier } of collectModuleReferencesFromSource(source)) {
|
||||
if (kind !== "dynamic-import") {
|
||||
continue;
|
||||
}
|
||||
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
|
||||
if (
|
||||
resolvedPath &&
|
||||
(/^src\/plugins\/runtime\/runtime-[^/]+\.ts$/.test(resolvedPath) ||
|
||||
/^extensions\/[^/]+\/runtime-api\.[^/]+$/.test(resolvedPath))
|
||||
) {
|
||||
pushEntry(entries, {
|
||||
category: "runtime-type-implementation-edge",
|
||||
file: relativeFile,
|
||||
line,
|
||||
kind: "import-type",
|
||||
specifier,
|
||||
resolvedPath,
|
||||
reason: "runtime type file references implementation shim directly",
|
||||
});
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return entries;
|
||||
}
|
||||
|
||||
function scanRuntimeServiceLocatorSmells(sourceFile, filePath) {
|
||||
function scanRuntimeServiceLocatorSmells(source, filePath) {
|
||||
const relativeFile = normalizeRepoPath(repoRoot, filePath);
|
||||
if (
|
||||
!relativeFile.startsWith("src/plugin-sdk/") &&
|
||||
@@ -120,47 +111,25 @@ function scanRuntimeServiceLocatorSmells(sourceFile, filePath) {
|
||||
const runtimeStoreCalls = [];
|
||||
const mutableStateNodes = [];
|
||||
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
||||
const isExported = statement.modifiers?.some(
|
||||
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
||||
);
|
||||
if (isExported) {
|
||||
exportedNames.add(statement.name.text);
|
||||
}
|
||||
} else if (ts.isVariableStatement(statement)) {
|
||||
const isExported = statement.modifiers?.some(
|
||||
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
||||
);
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name) && isExported) {
|
||||
exportedNames.add(declaration.name.text);
|
||||
}
|
||||
if (
|
||||
!isExported &&
|
||||
(statement.declarationList.flags & ts.NodeFlags.Let) !== 0 &&
|
||||
ts.isIdentifier(declaration.name)
|
||||
) {
|
||||
mutableStateNodes.push(declaration.name);
|
||||
}
|
||||
}
|
||||
const lines = source.split(/\r?\n/);
|
||||
for (const [index, line] of lines.entries()) {
|
||||
const lineNumber = index + 1;
|
||||
const exportedFunction = line.match(/^\s*export\s+function\s+([A-Za-z_$][\w$]*)/);
|
||||
if (exportedFunction) {
|
||||
exportedNames.add(exportedFunction[1]);
|
||||
}
|
||||
const exportedVariable = line.match(/^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/);
|
||||
if (exportedVariable) {
|
||||
exportedNames.add(exportedVariable[1]);
|
||||
}
|
||||
for (const mutableMatch of line.matchAll(/^\s*let\s+([A-Za-z_$][\w$]*)/g)) {
|
||||
mutableStateNodes.push({ line: lineNumber, text: mutableMatch[1] });
|
||||
}
|
||||
if (line.includes("createPluginRuntimeStore")) {
|
||||
runtimeStoreCalls.push({ line: lineNumber });
|
||||
}
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "createPluginRuntimeStore"
|
||||
) {
|
||||
runtimeStoreCalls.push(node.expression);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
|
||||
const getterNames = [...exportedNames].filter((name) => /^get[A-Z]/.test(name));
|
||||
const setterNames = [...exportedNames].filter((name) => /^set[A-Z]/.test(name));
|
||||
|
||||
@@ -169,7 +138,7 @@ function scanRuntimeServiceLocatorSmells(sourceFile, filePath) {
|
||||
pushEntry(entries, {
|
||||
category: "runtime-service-locator",
|
||||
file: relativeFile,
|
||||
line: toLine(sourceFile, callNode),
|
||||
line: callNode.line,
|
||||
kind: "runtime-store",
|
||||
specifier: "createPluginRuntimeStore",
|
||||
resolvedPath: relativeFile,
|
||||
@@ -183,7 +152,7 @@ function scanRuntimeServiceLocatorSmells(sourceFile, filePath) {
|
||||
pushEntry(entries, {
|
||||
category: "runtime-service-locator",
|
||||
file: relativeFile,
|
||||
line: toLine(sourceFile, identifier),
|
||||
line: identifier.line,
|
||||
kind: "mutable-state",
|
||||
specifier: identifier.text,
|
||||
resolvedPath: relativeFile,
|
||||
@@ -201,18 +170,16 @@ export async function collectArchitectureSmells() {
|
||||
const files = (await collectTypeScriptFilesFromRoots(scanRoots)).toSorted((left, right) =>
|
||||
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
|
||||
);
|
||||
return await collectTypeScriptInventory({
|
||||
ts,
|
||||
files,
|
||||
compareEntries,
|
||||
collectEntries(sourceFile, filePath) {
|
||||
return [
|
||||
...scanPluginSdkExtensionFacadeSmells(sourceFile, filePath),
|
||||
...scanRuntimeTypeImplementationSmells(sourceFile, filePath),
|
||||
...scanRuntimeServiceLocatorSmells(sourceFile, filePath),
|
||||
];
|
||||
},
|
||||
});
|
||||
const entriesByFile = await Promise.all(
|
||||
files.map(async (filePath) => {
|
||||
const source = await fs.readFile(filePath, "utf8");
|
||||
const entries = scanPluginSdkExtensionFacadeSmells(source, filePath);
|
||||
entries.push(...scanRuntimeTypeImplementationSmells(source, filePath));
|
||||
entries.push(...scanRuntimeServiceLocatorSmells(source, filePath));
|
||||
return entries;
|
||||
}),
|
||||
);
|
||||
return entriesByFile.flat().toSorted(compareEntries);
|
||||
})();
|
||||
try {
|
||||
return await architectureSmellsPromise;
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import {
|
||||
BUNDLED_PLUGIN_PATH_PREFIX,
|
||||
BUNDLED_PLUGIN_ROOT_DIR,
|
||||
} from "./lib/bundled-plugin-paths.mjs";
|
||||
import { classifyBundledExtensionSourcePath } from "./lib/extension-source-classifier.mjs";
|
||||
import {
|
||||
collectModuleReferencesFromSource,
|
||||
diffInventoryEntries,
|
||||
normalizeRepoPath,
|
||||
resolveRepoSpecifier,
|
||||
@@ -107,7 +107,7 @@ async function collectExtensionModuleReferences() {
|
||||
}
|
||||
return {
|
||||
filePath,
|
||||
references: collectModuleReferences(source),
|
||||
references: collectModuleReferencesFromSource(source),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -125,49 +125,6 @@ function mayContainModuleSpecifier(source) {
|
||||
);
|
||||
}
|
||||
|
||||
function collectModuleReferences(source) {
|
||||
const lineStarts = computeLineStarts(source);
|
||||
return ts.preProcessFile(source, true, true).importedFiles.map((reference) => ({
|
||||
kind: inferModuleReferenceKind(source, reference.pos),
|
||||
line: lineFromPosition(lineStarts, reference.pos),
|
||||
specifier: reference.fileName,
|
||||
}));
|
||||
}
|
||||
|
||||
function computeLineStarts(source) {
|
||||
const lineStarts = [0];
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
if (source.charCodeAt(index) === 10) {
|
||||
lineStarts.push(index + 1);
|
||||
}
|
||||
}
|
||||
return lineStarts;
|
||||
}
|
||||
|
||||
function lineFromPosition(lineStarts, position) {
|
||||
let low = 0;
|
||||
let high = lineStarts.length - 1;
|
||||
while (low <= high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
if (lineStarts[middle] <= position) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
return high + 1;
|
||||
}
|
||||
|
||||
function inferModuleReferenceKind(source, specifierStart) {
|
||||
const importIndex = source.lastIndexOf("import", specifierStart);
|
||||
const exportIndex = source.lastIndexOf("export", specifierStart);
|
||||
if (exportIndex > importIndex) {
|
||||
return "export";
|
||||
}
|
||||
const importPrefix = source.slice(importIndex, specifierStart);
|
||||
return /\bimport\s*\(\s*["']?$/.test(importPrefix) ? "dynamic-import" : "import";
|
||||
}
|
||||
|
||||
function resolveExtensionRoot(filePath) {
|
||||
const relativePath = normalizeRepoPath(repoRoot, filePath);
|
||||
const segments = relativePath.split("/");
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import ts from "typescript";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { BUNDLED_PLUGIN_PATH_PREFIX } from "./bundled-plugin-paths.mjs";
|
||||
import {
|
||||
collectTypeScriptInventory,
|
||||
collectModuleReferencesFromSource,
|
||||
createCachedAsync,
|
||||
formatGroupedInventoryHuman,
|
||||
normalizeRepoPath,
|
||||
resolveRepoSpecifier,
|
||||
visitModuleSpecifiers,
|
||||
writeLine,
|
||||
} from "./guard-inventory-utils.mjs";
|
||||
import {
|
||||
collectTypeScriptFilesFromRoots,
|
||||
resolveRepoRoot,
|
||||
resolveSourceRoots,
|
||||
toLine,
|
||||
} from "./ts-guard-utils.mjs";
|
||||
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
@@ -38,27 +36,29 @@ function classifyResolvedExtensionReason(kind, boundaryLabel) {
|
||||
return `${verb} bundled plugin file from ${boundaryLabel} boundary`;
|
||||
}
|
||||
|
||||
function scanImportBoundaryViolations(sourceFile, filePath, boundaryLabel, allowResolvedPath) {
|
||||
function scanImportBoundaryViolations(source, filePath, boundaryLabel, allowResolvedPath) {
|
||||
const entries = [];
|
||||
const relativeFile = normalizeRepoPath(repoRoot, filePath);
|
||||
|
||||
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifier, specifierNode }) => {
|
||||
for (const reference of collectModuleReferencesFromSource(source)) {
|
||||
const kind = reference.kind;
|
||||
const specifier = reference.specifier;
|
||||
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
|
||||
if (!resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (allowResolvedPath?.(resolvedPath, { kind, specifier, file: relativeFile })) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
file: relativeFile,
|
||||
line: toLine(sourceFile, specifierNode),
|
||||
line: reference.line,
|
||||
kind,
|
||||
specifier,
|
||||
resolvedPath,
|
||||
reason: classifyResolvedExtensionReason(kind, boundaryLabel),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
@@ -72,22 +72,25 @@ export function createExtensionImportBoundaryChecker(params) {
|
||||
.toSorted((left, right) =>
|
||||
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
|
||||
);
|
||||
return await collectTypeScriptInventory({
|
||||
ts,
|
||||
files,
|
||||
compareEntries,
|
||||
collectEntries(sourceFile, filePath) {
|
||||
const entriesByFile = await Promise.all(
|
||||
files.map(async (filePath) => {
|
||||
const source = await fs.readFile(filePath, "utf8");
|
||||
if (
|
||||
params.skipSourcesWithoutBundledPluginPrefix &&
|
||||
!source.includes(BUNDLED_PLUGIN_PATH_PREFIX)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return scanImportBoundaryViolations(
|
||||
sourceFile,
|
||||
source,
|
||||
filePath,
|
||||
params.boundaryLabel,
|
||||
params.allowResolvedPath,
|
||||
);
|
||||
},
|
||||
shouldParseSource: params.skipSourcesWithoutBundledPluginPrefix
|
||||
? (source) => source.includes(BUNDLED_PLUGIN_PATH_PREFIX)
|
||||
: undefined,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const inventory = entriesByFile.flat();
|
||||
return inventory.toSorted(compareEntries);
|
||||
});
|
||||
|
||||
async function main(argv = process.argv.slice(2), io) {
|
||||
|
||||
@@ -75,6 +75,122 @@ export function writeLine(stream, text) {
|
||||
stream.write(`${text}\n`);
|
||||
}
|
||||
|
||||
export function collectModuleReferencesFromSource(source) {
|
||||
const lineStarts = computeLineStarts(source);
|
||||
const isCodePosition = createCodePositionChecker(source);
|
||||
const references = [];
|
||||
const push = (kind, specifier, position, syntaxPosition) => {
|
||||
if (!isCodePosition(syntaxPosition)) {
|
||||
return;
|
||||
}
|
||||
references.push({
|
||||
kind,
|
||||
line: lineFromPosition(lineStarts, position),
|
||||
specifier,
|
||||
});
|
||||
};
|
||||
|
||||
for (const match of source.matchAll(/\bimport\s*\(\s*(["'])([^"']+)\1/g)) {
|
||||
push("dynamic-import", match[2], match.index + match[0].lastIndexOf(match[1]), match.index);
|
||||
}
|
||||
for (const match of source.matchAll(/^\s*import\s*(["'])([^"']+)\1/gm)) {
|
||||
push(
|
||||
"import",
|
||||
match[2],
|
||||
match.index + match[0].lastIndexOf(match[1]),
|
||||
match.index + match[0].indexOf("import"),
|
||||
);
|
||||
}
|
||||
for (const match of source.matchAll(
|
||||
/^\s*(import|export)\s+(?:type\s+)?[^;"']*?\bfrom\s*(["'])([^"']+)\2/gm,
|
||||
)) {
|
||||
push(
|
||||
match[1],
|
||||
match[3],
|
||||
match.index + match[0].lastIndexOf(match[2]),
|
||||
match.index + match[0].indexOf(match[1]),
|
||||
);
|
||||
}
|
||||
|
||||
return references.toSorted(
|
||||
(left, right) =>
|
||||
left.line - right.line ||
|
||||
left.kind.localeCompare(right.kind) ||
|
||||
left.specifier.localeCompare(right.specifier),
|
||||
);
|
||||
}
|
||||
|
||||
function createCodePositionChecker(source) {
|
||||
const codePositions = new Uint8Array(source.length);
|
||||
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
const next = source[index + 1];
|
||||
|
||||
if (char === "/" && next === "/") {
|
||||
index += 2;
|
||||
while (index < source.length && source.charCodeAt(index) !== 10) {
|
||||
index += 1;
|
||||
}
|
||||
index -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "*") {
|
||||
index += 2;
|
||||
while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) {
|
||||
index += 1;
|
||||
}
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "'" || char === '"' || char === "`") {
|
||||
const quote = char;
|
||||
index += 1;
|
||||
while (index < source.length) {
|
||||
if (source[index] === "\\") {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if (source[index] === quote) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
codePositions[index] = 1;
|
||||
}
|
||||
|
||||
return (position) => codePositions[position] === 1;
|
||||
}
|
||||
|
||||
function computeLineStarts(source) {
|
||||
const lineStarts = [0];
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
if (source.charCodeAt(index) === 10) {
|
||||
lineStarts.push(index + 1);
|
||||
}
|
||||
}
|
||||
return lineStarts;
|
||||
}
|
||||
|
||||
function lineFromPosition(lineStarts, position) {
|
||||
let low = 0;
|
||||
let high = lineStarts.length - 1;
|
||||
while (low <= high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
if (lineStarts[middle] <= position) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
return high + 1;
|
||||
}
|
||||
|
||||
export function createCachedAsync(factory) {
|
||||
let cachedPromise = null;
|
||||
return async function getCachedValue() {
|
||||
|
||||
Reference in New Issue
Block a user