mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
fc712b3750
A release call was linked to its acquisition through the receiver's lexical
binding alone, so `stream = { destroy() {} }; stream.destroy()` still
produced a resolved `releases` edge to the original stream and entered the
default ownership trace.
A release is now complete only when every value written to the handle
binding is the linked acquisition itself (through parentheses, TypeScript
wrappers or `await`) or a nullish placeholder such as `let h = null`, which
cannot be released. Any other write, conditional replacement, or
destructuring keeps the linked release as a candidate (partial).
Fixes #3313
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
6.5 KiB
TypeScript
192 lines
6.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import {
|
|
BUILTIN_RESOURCE_SPECIFIERS,
|
|
BUILTIN_RESOURCE_METHODS,
|
|
} from "./javascriptBuiltinResourceMethods.js";
|
|
import type {
|
|
JavaScriptSemanticCallable,
|
|
JavaScriptSemanticResourceOperation,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
semanticStaticPropertyName,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticBindingState,
|
|
} from "./javascriptSemanticState.js";
|
|
import {
|
|
builtinDataEffectMethod,
|
|
dataEffectMemberCallee,
|
|
outerDataEffectBinding,
|
|
traverseDataEffects,
|
|
type DataEffectTraversalContext,
|
|
} from "./javascriptSemanticDataEffectHelpers.js";
|
|
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
|
|
|
const RELEASE_METHODS = ["close", "destroy", "end"] as const;
|
|
|
|
interface ResourceCandidate {
|
|
readonly node: t.CallExpression | t.OptionalCallExpression;
|
|
readonly method: JavaScriptSemanticResourceOperation["method"];
|
|
readonly ownerCallableId: string | null;
|
|
readonly resultBindingId: string | null;
|
|
}
|
|
|
|
/** Recover built-in filesystem/network acquisition and local release handles. */
|
|
export const collectJavaScriptSemanticResources = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
): JavaScriptSemanticResourceOperation[] => {
|
|
const context: DataEffectTraversalContext = {
|
|
state,
|
|
callableIds: new Set(callables.map(({ callableId }) => callableId)),
|
|
callableStack: [],
|
|
ancestors: [],
|
|
};
|
|
const acquisitions = collectAcquisitions(program, context);
|
|
return [
|
|
...acquisitions.map(immutableAcquisition),
|
|
...collectReleases(program, context, acquisitions),
|
|
];
|
|
};
|
|
|
|
const collectAcquisitions = (
|
|
program: t.Program,
|
|
context: DataEffectTraversalContext,
|
|
): ResourceCandidate[] => {
|
|
const output: ResourceCandidate[] = [];
|
|
traverseDataEffects(program, context, (node) => {
|
|
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
|
|
const method = acquisitionMethod(node.callee, context.state);
|
|
if (method === null) return;
|
|
output.push({
|
|
node,
|
|
method,
|
|
ownerCallableId: context.callableStack.at(-1) ?? null,
|
|
resultBindingId: outerDataEffectBinding(node, context),
|
|
});
|
|
});
|
|
return output;
|
|
};
|
|
|
|
const collectReleases = (
|
|
program: t.Program,
|
|
context: DataEffectTraversalContext,
|
|
acquisitions: readonly ResourceCandidate[],
|
|
): JavaScriptSemanticResourceOperation[] => {
|
|
const byBinding = new Map<string, ResourceCandidate[]>();
|
|
for (const acquisition of acquisitions) {
|
|
if (acquisition.resultBindingId === null) continue;
|
|
const existing = byBinding.get(acquisition.resultBindingId) ?? [];
|
|
existing.push(acquisition);
|
|
byBinding.set(acquisition.resultBindingId, existing);
|
|
}
|
|
const output: JavaScriptSemanticResourceOperation[] = [];
|
|
traverseDataEffects(program, context, (node) => {
|
|
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
|
|
const member = dataEffectMemberCallee(node);
|
|
if (member === null || !t.isIdentifier(member.object)) return;
|
|
const method = RELEASE_METHODS.find(
|
|
(candidate) =>
|
|
candidate ===
|
|
semanticStaticPropertyName(member.property, member.computed),
|
|
);
|
|
if (method === undefined) return;
|
|
const binding = resolveSemanticBindingState(
|
|
context.state,
|
|
member.object,
|
|
member.object.name,
|
|
);
|
|
const linked =
|
|
binding === undefined ? [] : (byBinding.get(binding.bindingId) ?? []);
|
|
if (binding === undefined || linked.length === 0) return;
|
|
output.push({
|
|
resourceId: `resource:release:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
|
|
kind: "release",
|
|
method,
|
|
location: range(node),
|
|
ownerCallableId: context.callableStack.at(-1) ?? null,
|
|
resultBindingId: binding.bindingId,
|
|
linkedResourceIds: linked.map(resourceCandidateId).sort(),
|
|
resolution:
|
|
linked.length === 1 &&
|
|
holdsOnlyAcquisitions(binding, linked, context.state)
|
|
? "complete"
|
|
: "partial",
|
|
});
|
|
});
|
|
return output;
|
|
};
|
|
|
|
/**
|
|
* A lexical handle identifies its acquisition only while every value written
|
|
* to the binding is that acquisition itself. Any replacement, even a
|
|
* conditional or later one, leaves the released resource a candidate. A
|
|
* nullish placeholder, as in `let h = null; try { h = open() } finally {
|
|
* h?.close() }`, releases nothing else: the call is skipped or throws.
|
|
*/
|
|
const holdsOnlyAcquisitions = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
linked: readonly ResourceCandidate[],
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
[...binding.initializers, ...binding.referenceInitializers].every(
|
|
({ node, projection }) => {
|
|
if (projection.length > 0) return false;
|
|
let value = unwrapJavaScriptExpression(node).node;
|
|
if (t.isAwaitExpression(value))
|
|
value = unwrapJavaScriptExpression(value.argument).node;
|
|
return (
|
|
isNullishPlaceholder(value, state) ||
|
|
linked.some((acquisition) => acquisition.node === value)
|
|
);
|
|
},
|
|
);
|
|
|
|
const isNullishPlaceholder = (
|
|
value: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
t.isNullLiteral(value) ||
|
|
t.isUnaryExpression(value, { operator: "void" }) ||
|
|
isUnmodifiedGlobalReference(value, state, "undefined");
|
|
|
|
const immutableAcquisition = (
|
|
candidate: ResourceCandidate,
|
|
): JavaScriptSemanticResourceOperation => ({
|
|
resourceId: resourceCandidateId(candidate),
|
|
kind: "acquire",
|
|
method: candidate.method,
|
|
location: range(candidate.node),
|
|
ownerCallableId: candidate.ownerCallableId,
|
|
resultBindingId: candidate.resultBindingId,
|
|
linkedResourceIds: [],
|
|
resolution: "complete",
|
|
});
|
|
|
|
const acquisitionMethod = (
|
|
callee: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptSemanticResourceOperation["method"] | null => {
|
|
for (const specifier of BUILTIN_RESOURCE_SPECIFIERS) {
|
|
const method = builtinDataEffectMethod(callee, state, [specifier]);
|
|
if (
|
|
method !== null &&
|
|
(
|
|
BUILTIN_RESOURCE_METHODS[
|
|
specifier as keyof typeof BUILTIN_RESOURCE_METHODS
|
|
] as readonly string[]
|
|
).includes(method)
|
|
)
|
|
return method as JavaScriptSemanticResourceOperation["method"];
|
|
}
|
|
return null;
|
|
};
|
|
const resourceCandidateId = (candidate: ResourceCandidate): string =>
|
|
`resource:acquire:${String(candidate.node.start ?? -1)}:${String(candidate.node.end ?? -1)}`;
|