mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 15:01:41 +00:00
175 lines
5.9 KiB
JavaScript
175 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import ts from "typescript";
|
|
import {
|
|
collectFileViolations,
|
|
resolveRepoRoot,
|
|
resolveSourceRoots,
|
|
runAsScript,
|
|
toLine,
|
|
unwrapExpression,
|
|
} from "./lib/ts-guard-utils.mjs";
|
|
|
|
const removedAsyncTransactionNames = new Set([
|
|
"runOpenClawAgentWriteTransactionAsync",
|
|
"runSqliteImmediateTransactionAsync",
|
|
]);
|
|
const synchronousTransactionCallbackIndexes = new Map([
|
|
["runOpenClawAgentWriteTransaction", 0],
|
|
["runOpenClawStateWriteTransaction", 0],
|
|
["runSqliteImmediateTransactionSync", 1],
|
|
]);
|
|
|
|
function expressionName(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return unwrapped.text;
|
|
}
|
|
if (ts.isPropertyAccessExpression(unwrapped)) {
|
|
return unwrapped.name.text;
|
|
}
|
|
if (ts.isElementAccessExpression(unwrapped) && ts.isStringLiteral(unwrapped.argumentExpression)) {
|
|
return unwrapped.argumentExpression.text;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function hasAsyncModifier(node) {
|
|
return node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true;
|
|
}
|
|
|
|
function collectLocalAsyncFunctionNames(sourceFile) {
|
|
const names = new Set();
|
|
const visit = (node) => {
|
|
if (ts.isFunctionDeclaration(node) && node.name && hasAsyncModifier(node)) {
|
|
names.add(node.name.text);
|
|
}
|
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
const initializer = unwrapExpression(node.initializer);
|
|
if (
|
|
(ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) &&
|
|
hasAsyncModifier(initializer)
|
|
) {
|
|
names.add(node.name.text);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return names;
|
|
}
|
|
|
|
function collectSynchronousTransactionAliases(sourceFile) {
|
|
const aliases = new Map();
|
|
for (const statement of sourceFile.statements) {
|
|
if (!ts.isImportDeclaration(statement)) {
|
|
continue;
|
|
}
|
|
const bindings = statement.importClause?.namedBindings;
|
|
if (!bindings || !ts.isNamedImports(bindings)) {
|
|
continue;
|
|
}
|
|
for (const specifier of bindings.elements) {
|
|
const importedName = specifier.propertyName?.text ?? specifier.name.text;
|
|
if (synchronousTransactionCallbackIndexes.has(importedName)) {
|
|
aliases.set(specifier.name.text, importedName);
|
|
}
|
|
}
|
|
}
|
|
return aliases;
|
|
}
|
|
|
|
function isAsyncCallback(expression, localAsyncFunctionNames) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
|
|
return hasAsyncModifier(unwrapped);
|
|
}
|
|
return ts.isIdentifier(unwrapped) && localAsyncFunctionNames.has(unwrapped.text);
|
|
}
|
|
|
|
export function findSqliteTransactionBoundaryViolations(content, fileName = "source.ts") {
|
|
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
|
const localAsyncFunctionNames = collectLocalAsyncFunctionNames(sourceFile);
|
|
const transactionAliases = collectSynchronousTransactionAliases(sourceFile);
|
|
const violations = [];
|
|
|
|
const visit = (node) => {
|
|
if (ts.isImportDeclaration(node)) {
|
|
const bindings = node.importClause?.namedBindings;
|
|
if (bindings && ts.isNamedImports(bindings)) {
|
|
for (const specifier of bindings.elements) {
|
|
const importedName = specifier.propertyName?.text ?? specifier.name.text;
|
|
if (removedAsyncTransactionNames.has(importedName)) {
|
|
violations.push({
|
|
line: toLine(sourceFile, specifier),
|
|
reason: `imports removed async SQLite transaction primitive "${importedName}"`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
((ts.isFunctionDeclaration(node) && node.name) ||
|
|
(ts.isVariableDeclaration(node) && ts.isIdentifier(node.name))) &&
|
|
removedAsyncTransactionNames.has(node.name.text)
|
|
) {
|
|
violations.push({
|
|
line: toLine(sourceFile, node.name),
|
|
reason: `declares removed async SQLite transaction primitive "${node.name.text}"`,
|
|
});
|
|
}
|
|
|
|
if (ts.isCallExpression(node)) {
|
|
const calledName = expressionName(node.expression);
|
|
if (calledName && removedAsyncTransactionNames.has(calledName)) {
|
|
violations.push({
|
|
line: toLine(sourceFile, node.expression),
|
|
reason: `calls removed async SQLite transaction primitive "${calledName}"`,
|
|
});
|
|
}
|
|
const canonicalName = calledName
|
|
? (transactionAliases.get(calledName) ?? calledName)
|
|
: undefined;
|
|
const callbackIndex = canonicalName
|
|
? synchronousTransactionCallbackIndexes.get(canonicalName)
|
|
: undefined;
|
|
const callback = callbackIndex === undefined ? undefined : node.arguments[callbackIndex];
|
|
if (canonicalName && callback && isAsyncCallback(callback, localAsyncFunctionNames)) {
|
|
violations.push({
|
|
line: toLine(sourceFile, callback),
|
|
reason: `passes an async callback to synchronous SQLite transaction helper "${canonicalName}"`,
|
|
});
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return violations;
|
|
}
|
|
|
|
async function main() {
|
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
const violations = await collectFileViolations({
|
|
repoRoot,
|
|
sourceRoots: resolveSourceRoots(repoRoot, ["src", "extensions", "packages"]),
|
|
skipFile: (filePath) => /(^|[\\/])dist([\\/]|$)/.test(filePath),
|
|
findViolations: findSqliteTransactionBoundaryViolations,
|
|
});
|
|
if (violations.length === 0) {
|
|
console.log("SQLite transaction boundary guard passed.");
|
|
return;
|
|
}
|
|
console.error("Found asynchronous SQLite transaction work:");
|
|
for (const violation of violations) {
|
|
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
|
|
}
|
|
console.error(
|
|
"Complete asynchronous preparation before the transaction, then validate and apply inside a synchronous transaction callback.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
runAsScript(import.meta.url, main);
|