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 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-20 15:27:17 +00:00
1 parent 080c0e2b2b
commit ad3d995604
3 files changed
+100 -7

No files matched your search

@@ -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;
@@ -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, {
@@ -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 },
});
};