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(); 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)}`;