From ad3d9956047bcd00baaf4e96455a76c7d9c7b90d Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:27:17 +0000 Subject: [PATCH] fix(javascript): carry a directly returned value's literal to its return site Return sites evaluated their returned expression but only projected identifier references as aliases. A function that returned a member read such as `settings.index` linked only the receiver `settings` as a candidate alias, so backward provenance from the call result lost the known literal and reported ambiguity. Project a non-identifier return's evaluated primitive onto its return site, as binding values already are, and do not offer the expression's inner references as candidates when that value is one exact literal. Fixes #5585 Co-Authored-By: Claude Opus 5.5 --- .../JavaScriptSemanticGraphBuilder.test.ts | 31 +++++++++ .../JavaScriptSemanticGraphFlowProjection.ts | 8 +++ .../JavaScriptSemanticGraphValueProjection.ts | 68 +++++++++++++++++-- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts index c3c4fea0fc..1ee31d0589 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts @@ -94,6 +94,37 @@ it.each([ }, ); +it.each([false, true])( + "traces a directly returned member read back to its literal (candidates: %s)", + (candidates) => { + const graph = graphFor(` + const settings = { index: "products" }; + function viaLocal() { const value = settings.index; return value; } + function viaMember() { return settings.index; } + const a = viaLocal(); + const b = viaMember(); + `); + for (const label of ["a", "b"]) { + const binding = graph.nodes.find( + (node) => node.kind === "binding" && node.label === label, + ); + if (binding === undefined) throw new Error(`Expected binding ${label}`); + const trace = queryJavaScriptSemanticGraph(graph, { + seed: { kind: "semantic-node", node_id: binding.node_id }, + direction: "backward-provenance", + include_ambiguous_dynamic_edges: candidates, + }); + expect(trace.status, label).toBe("found"); + expect( + trace.nodes.filter(({ kind }) => kind === "literal"), + label, + ).toEqual([ + expect.objectContaining({ properties: { value: "products" } }), + ]); + } + }, +); + it("projects closure and direct interprocedural flow without execution", () => { const graph = graphFor(` const outer = 2; diff --git a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts index 052a132618..f185d3ffd8 100644 --- a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts @@ -197,6 +197,14 @@ export const projectSemanticReturnValues = ( for (const callable of context.ir.callables) for (const site of callable.returnSites) { const returnNode = context.returnSiteNodes.get(site.returnSiteId); + // A non-identifier return evaluated to one exact primitive is defined by + // its literal (see projectSemanticValues); references inside it, such as + // a member read's receiver, are not candidates for the returned value. + if ( + site.identityReferenceLocation === null && + site.value.status === "literal" + ) + continue; const references = context.referenceNodesWithin(site.location); for (const reference of references) addSemanticGraphRelation(context.state, { diff --git a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts index eac56882a7..81f51bd1cf 100644 --- a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts @@ -34,6 +34,34 @@ export const projectSemanticValues = ( path: [], }); } + // A return through an identifier already reaches its value through the + // resolved binding alias. Any other returned expression, such as a member + // read, carries its evaluated primitive on the return site itself. + for (const callable of context.ir.callables) + for (const site of callable.returnSites) { + const returnNode = context.returnSiteNodes.get(site.returnSiteId); + if (returnNode === undefined || site.identityReferenceLocation !== null) + continue; + const primitives = + site.value.status === "literal" + ? [site.value.value] + : site.value.status === "union" + ? site.value.values + : []; + for (const primitive of primitives) + addSemanticGraphRelation(context.state, { + source: addLiteralNode(context, { + key: `return:${site.returnSiteId}`, + location: site.location, + functionNodeId: returnNode.function_node_id, + value: primitive, + }), + target: returnNode, + relation: "defines", + resolution: + site.value.status === "literal" ? "resolved" : "candidate", + }); + } }; interface ValueProjectionInput { @@ -50,7 +78,10 @@ const projectValue = (input: ValueProjectionInput): void => { path.length === 0 ? "binding" : `property:${semanticPropertyPointer(path)}`; if (target === null) return; if (value.status === "literal") { - const literal = addLiteralNode(context, binding, value.value, role); + const literal = addLiteralNode( + context, + bindingLiteral(context, binding, value.value, role), + ); addSemanticGraphRelation(context.state, { source: literal, target, @@ -59,7 +90,10 @@ const projectValue = (input: ValueProjectionInput): void => { }); } else if (value.status === "union") for (const primitive of value.values) { - const literal = addLiteralNode(context, binding, primitive, role); + const literal = addLiteralNode( + context, + bindingLiteral(context, binding, primitive, role), + ); addSemanticGraphRelation(context.state, { source: literal, target, @@ -134,11 +168,32 @@ const projectValue = (input: ValueProjectionInput): void => { } }; -const addLiteralNode = ( +interface LiteralOwner { + /** Stable owner and role, such as a binding slot or a return site. */ + readonly key: string; + readonly location: + | JavaScriptSemanticBinding["definitions"][number]["location"] + | null; + readonly functionNodeId: string | null; + readonly value: string | number | boolean | null; +} + +const bindingLiteral = ( context: SemanticFlowProjectionContext, binding: JavaScriptSemanticBinding, value: string | number | boolean | null, role: string, +): LiteralOwner => ({ + key: `${binding.bindingId}:${role}`, + location: binding.definitions[0]?.location ?? null, + functionNodeId: + context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null, + value, +}); + +const addLiteralNode = ( + context: SemanticFlowProjectionContext, + { key, location, functionNodeId, value }: LiteralOwner, ) => { const encodedValue = JSON.stringify(value); const valueDigest = `value-sha256:${digestCanonicalValue(value, "Semantic literal")}`; @@ -149,14 +204,13 @@ const addLiteralNode = ( kind: "literal", // Identity needs an exact value commitment, not another payload copy. // Keep the complete literal in properties for queries and Evidence export. - roleKey: `literal:${binding.bindingId}:${role}:${valueKey}`, - location: binding.definitions[0]?.location ?? null, + roleKey: `literal:${key}:${valueKey}`, + location, label: typeof value === "string" && encodedValue.length > stringLabel.length ? stringLabel : encodedValue, - functionNodeId: - context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null, + functionNodeId, properties: { value }, }); };