mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
fix(javascript): match resource release methods to the acquired handle (#7773)
* fix(javascript): match resource release methods to the acquired handle
Every acquisition family admitted every release method, so `h.close()`,
`h.destroy()` and `h.end()` each resolved as a complete release of whatever the
binding held. Node exposes a different subset per acquisition, and the rejected
pairs throw at the call site instead.
Measured on Node 24.22.1:
| acquisition | value | release members |
| --- | --- | --- |
| `fs.openSync` | number | none |
| `fs.open` (callback) | undefined | none |
| `fs.createReadStream` | ReadStream | `close`, `destroy` (`end` is the byte-offset field) |
| `fs.createWriteStream` | WriteStream | `close`, `destroy`, `end` |
| `fs/promises.open` | FileHandle | `close` |
| `net.connect` | Socket | `destroy`, `end` |
| `tls.connect` | TLSSocket | `destroy`, `end` |
So `fs.openSync(...)` + `fd.close()`, `createReadStream(...)` + `.end()`,
`net.connect(...)` + `.close()` and the FileHandle cases emitted a `releases` /
`resolved` fact for a call that throws.
Carry the specifier with each acquisition candidate, list the release methods
each pair actually has, and emit no release when the pair is not one of them. A
release stays `complete` only when the binding held exactly one acquisition and
that acquisition admitted the method.
`tls.createConnection` is not a Node export at all: `'createConnection' in
require('node:tls')` is false and the ESM import does not link. Drop it from the
table so a `tls.createConnection` call yields no acquisition.
* chore: retrigger title and readiness checks after the title was corrected
* fix(javascript): cover a mixed-family binding before completing a release
greptile asked for the mixed-family case: a binding that can hold either a
FileHandle or a socket, then calls close(). The FileHandle is the only linked
acquisition and the release stays partial.
That case also showed the `candidates.length === 1` guard was dead: an
acquisition of another family is not in `linked`, so `holdsOnlyAcquisitions`
already returns false for it and the release already stayed partial without the
guard. Removing the guard and keeping the test means the behaviour is now pinned
by a test rather than by two overlapping conditions.
This commit is contained in:
1 parent
a965cbab06
commit
b7442c1958
3 files changed
+196
-8
No files matched your search
@@ -3,6 +3,7 @@ import * as t from "@babel/types";
|
||||
import {
|
||||
BUILTIN_RESOURCE_SPECIFIERS,
|
||||
BUILTIN_RESOURCE_METHODS,
|
||||
BUILTIN_RESOURCE_RELEASE_METHODS,
|
||||
} from "./javascriptBuiltinResourceMethods.js";
|
||||
import type {
|
||||
JavaScriptSemanticCallable,
|
||||
@@ -29,8 +30,23 @@ import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
const RELEASE_METHODS = ["close", "destroy", "end"] as const;
|
||||
|
||||
/**
|
||||
* Release methods the value from this acquisition actually has. An unlisted
|
||||
* specifier/method pair admits none, so a call to it emits no release fact.
|
||||
*/
|
||||
const releaseMethodsFor = (
|
||||
acquisition: ResourceCandidate,
|
||||
): readonly ("close" | "destroy" | "end")[] => {
|
||||
const key = `${acquisition.specifier}:${acquisition.method}`;
|
||||
const table: Readonly<
|
||||
Record<string, readonly ("close" | "destroy" | "end")[]>
|
||||
> = BUILTIN_RESOURCE_RELEASE_METHODS;
|
||||
return table[key] ?? [];
|
||||
};
|
||||
|
||||
interface ResourceCandidate {
|
||||
readonly node: t.CallExpression | t.OptionalCallExpression;
|
||||
readonly specifier: string;
|
||||
readonly method: JavaScriptSemanticResourceOperation["method"];
|
||||
readonly ownerCallableId: string | null;
|
||||
readonly resultBindingId: string | null;
|
||||
@@ -62,11 +78,12 @@ const collectAcquisitions = (
|
||||
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;
|
||||
const reference = acquisitionMethod(node.callee, context.state);
|
||||
if (reference === null) return;
|
||||
output.push({
|
||||
node,
|
||||
method,
|
||||
specifier: reference.specifier,
|
||||
method: reference.method,
|
||||
ownerCallableId: context.callableStack.at(-1) ?? null,
|
||||
resultBindingId: outerDataEffectBinding(node, context),
|
||||
});
|
||||
@@ -102,9 +119,15 @@ const collectReleases = (
|
||||
member.object,
|
||||
member.object.name,
|
||||
);
|
||||
const linked =
|
||||
const candidates =
|
||||
binding === undefined ? [] : (byBinding.get(binding.bindingId) ?? []);
|
||||
if (binding === undefined || linked.length === 0) return;
|
||||
if (binding === undefined || candidates.length === 0) return;
|
||||
const linked = candidates.filter((acquisition) =>
|
||||
releaseMethodsFor(acquisition).includes(method),
|
||||
);
|
||||
// No linked acquisition exposes this release method, so the call is not a
|
||||
// release of it at all: Node throws at the call site instead.
|
||||
if (linked.length === 0) return;
|
||||
output.push({
|
||||
resourceId: `resource:release:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
|
||||
kind: "release",
|
||||
@@ -169,10 +192,15 @@ const immutableAcquisition = (
|
||||
resolution: "complete",
|
||||
});
|
||||
|
||||
interface AcquisitionReference {
|
||||
readonly specifier: string;
|
||||
readonly method: JavaScriptSemanticResourceOperation["method"];
|
||||
}
|
||||
|
||||
const acquisitionMethod = (
|
||||
callee: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticResourceOperation["method"] | null => {
|
||||
): AcquisitionReference | null => {
|
||||
for (const specifier of BUILTIN_RESOURCE_SPECIFIERS) {
|
||||
const method = builtinDataEffectMethod(callee, state, [specifier]);
|
||||
if (
|
||||
@@ -183,7 +211,10 @@ const acquisitionMethod = (
|
||||
] as readonly string[]
|
||||
).includes(method)
|
||||
)
|
||||
return method as JavaScriptSemanticResourceOperation["method"];
|
||||
return {
|
||||
specifier,
|
||||
method: method as JavaScriptSemanticResourceOperation["method"],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user