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