mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
201 lines
6.4 KiB
JavaScript
201 lines
6.4 KiB
JavaScript
import { findBinding } from "./bindings.mjs";
|
|
|
|
/**
|
|
* Require an explicit cancellation reason when a directly checked caller signal
|
|
* is converted to an AnalysisCancelledError. This is deliberately syntactic:
|
|
* internal shutdown and cancellation forwarded through other owners are outside
|
|
* the rule.
|
|
*/
|
|
|
|
const coreModule = /(?:^|\/)analysisErrorCore(?:\.js|\.ts)?$/;
|
|
|
|
const unwrapChain = (node) =>
|
|
node?.type === "ChainExpression" ? node.expression : node;
|
|
|
|
const propertyName = (property) =>
|
|
property.computed
|
|
? property.property.type === "Literal"
|
|
? property.property.value
|
|
: undefined
|
|
: property.property.name;
|
|
|
|
const annotationType = (node) => {
|
|
const annotation = node?.typeAnnotation?.typeAnnotation;
|
|
return annotation?.type === "TSTypeReference" &&
|
|
annotation.typeName.type === "Identifier"
|
|
? annotation.typeName
|
|
: undefined;
|
|
};
|
|
|
|
const soleDefinition = (variable) =>
|
|
variable?.defs.length === 1 ? variable.defs[0] : undefined;
|
|
|
|
const isKnownType = (node, kind, context) => {
|
|
const type = annotationType(node);
|
|
if (type === undefined) return false;
|
|
const definition = soleDefinition(findBinding(context.sourceCode, type));
|
|
if (kind === "AbortSignal")
|
|
return type.name === "AbortSignal" && definition === undefined;
|
|
return (
|
|
definition?.type === "ImportBinding" &&
|
|
definition.parent?.type === "ImportDeclaration" &&
|
|
/(?:^|\/)AnalysisProvider(?:\.js|\.ts)?$/.test(
|
|
definition.parent.source.value,
|
|
) &&
|
|
definition.node.type === "ImportSpecifier" &&
|
|
definition.node.imported.name === "ExecutionOptions"
|
|
);
|
|
};
|
|
|
|
const unmodified = (variable) =>
|
|
variable !== undefined &&
|
|
variable.references.every(
|
|
(reference) => !reference.isWrite() || reference.init,
|
|
);
|
|
|
|
const isOptionsSignal = (node, context, seen) => {
|
|
node = unwrapChain(node);
|
|
if (node?.type !== "MemberExpression" || propertyName(node) !== "signal")
|
|
return false;
|
|
return isKnownBinding(node.object, "ExecutionOptions", context, seen);
|
|
};
|
|
|
|
const isKnownBinding = (node, kind, context, seen) => {
|
|
if (node?.type !== "Identifier") return false;
|
|
const variable = findBinding(context.sourceCode, node);
|
|
if (!unmodified(variable) || seen.has(variable)) return false;
|
|
seen.add(variable);
|
|
const definition = soleDefinition(variable);
|
|
if (definition?.type === "Parameter")
|
|
return isKnownType(definition.name, kind, context);
|
|
if (definition?.type !== "Variable" || definition.parent?.kind !== "const")
|
|
return false;
|
|
const declaration = definition.node;
|
|
if (isKnownType(declaration.id, kind, context)) return true;
|
|
return kind === "AbortSignal"
|
|
? isKnownSignal(declaration.init, context, seen)
|
|
: false;
|
|
};
|
|
|
|
const isKnownSignal = (node, context, seen = new Set()) =>
|
|
isKnownBinding(unwrapChain(node), "AbortSignal", context, seen) ||
|
|
isOptionsSignal(node, context, seen);
|
|
|
|
const isAbortRead = (node, context) => {
|
|
node = unwrapChain(node);
|
|
return (
|
|
node?.type === "MemberExpression" &&
|
|
propertyName(node) === "aborted" &&
|
|
isKnownSignal(node.object, context)
|
|
);
|
|
};
|
|
|
|
const isTrueLiteral = (node) => node?.type === "Literal" && node.value === true;
|
|
|
|
const positiveAbortGuard = (test, context) => {
|
|
if (isAbortRead(test, context)) return true;
|
|
if (
|
|
test?.type === "BinaryExpression" &&
|
|
(test.operator === "===" || test.operator === "==")
|
|
)
|
|
return (
|
|
(isAbortRead(test.left, context) && isTrueLiteral(test.right)) ||
|
|
(isTrueLiteral(test.left) && isAbortRead(test.right, context))
|
|
);
|
|
if (test?.type === "LogicalExpression" && test.operator === "&&")
|
|
return (
|
|
positiveAbortGuard(test.left, context) ||
|
|
positiveAbortGuard(test.right, context)
|
|
);
|
|
return false;
|
|
};
|
|
|
|
const isCanonicalConstructor = (node, context) => {
|
|
if (node?.type !== "Identifier") return false;
|
|
const definition = soleDefinition(findBinding(context.sourceCode, node));
|
|
return (
|
|
definition?.type === "ImportBinding" &&
|
|
definition.parent?.type === "ImportDeclaration" &&
|
|
coreModule.test(definition.parent.source.value) &&
|
|
definition.node.type === "ImportSpecifier" &&
|
|
definition.node.imported.name === "AnalysisCancelledError"
|
|
);
|
|
};
|
|
|
|
const hasExplicitCause = (node) => {
|
|
const options = node.arguments[1];
|
|
if (options !== undefined && options.type !== "ObjectExpression") return true; // Forwarded context is owned elsewhere.
|
|
if (options === undefined) return false;
|
|
return options.properties.some(
|
|
(property) =>
|
|
property.type === "SpreadElement" ||
|
|
(property.type === "Property" &&
|
|
(property.computed
|
|
? property.key.type !== "Literal" || property.key.value === "cause"
|
|
: (property.key.type === "Identifier"
|
|
? property.key.name
|
|
: property.key.value) === "cause")),
|
|
);
|
|
};
|
|
|
|
const inspectExpression = (expression, context) => {
|
|
if (expression?.type === "NewExpression") {
|
|
if (
|
|
isCanonicalConstructor(expression.callee, context) &&
|
|
!hasExplicitCause(expression)
|
|
)
|
|
context.report({
|
|
node: expression,
|
|
messageId: "missingCause",
|
|
});
|
|
return;
|
|
}
|
|
if (expression?.type === "CallExpression") {
|
|
// Common Result adapter: return err(new AnalysisCancelledError(...)).
|
|
for (const argument of expression.arguments)
|
|
inspectExpression(argument, context);
|
|
} else if (expression?.type === "AssignmentExpression") {
|
|
inspectExpression(expression.right, context);
|
|
}
|
|
};
|
|
|
|
const inspectBranch = (branch, context) => {
|
|
if (branch.type === "BlockStatement") {
|
|
for (const statement of branch.body) inspectBranch(statement, context);
|
|
} else if (branch.type === "VariableDeclaration") {
|
|
for (const declaration of branch.declarations)
|
|
inspectExpression(declaration.init, context);
|
|
} else if (
|
|
branch.type === "ThrowStatement" ||
|
|
branch.type === "ReturnStatement"
|
|
) {
|
|
inspectExpression(branch.argument, context);
|
|
} else if (branch.type === "ExpressionStatement") {
|
|
inspectExpression(branch.expression, context);
|
|
}
|
|
};
|
|
|
|
const cancellationCauseRule = {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description:
|
|
"Preserve a directly checked caller AbortSignal reason in AnalysisCancelledError",
|
|
},
|
|
messages: {
|
|
missingCause:
|
|
"Pass the checked AbortSignal's reason as an explicit cancellation cause.",
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
IfStatement(node) {
|
|
if (positiveAbortGuard(node.test, context))
|
|
inspectBranch(node.consequent, context);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
export default cancellationCauseRule;
|