fix(javascript): trace boundary results to assigned bindings (#7912)
This commit is contained in:
1 parent
2129c17925
commit
794907fa3d
5 files changed
+166
-1
No files matched your search
@@ -198,6 +198,10 @@ rereading mutable files. A seed may identify a semantic or application node,
|
||||
literal, function fingerprint, property, endpoint, event, or boundary field.
|
||||
For a recovered parse, coercion, or validation boundary, its field value is the
|
||||
static operation's method name (for example, `Number`).
|
||||
When a recovered boundary call directly supplies a scalar binding, the graph
|
||||
connects the boundary to that binding; unresolved boundary calls remain
|
||||
candidates. A boundary nested inside another expression or compound assignment
|
||||
does not establish a direct result binding.
|
||||
The query declares backward provenance, forward influence, callers, or
|
||||
ownership and can restrict admitted relation kinds. REA returns every matching
|
||||
seed and the complete reachable subgraph; callers do not choose node, relation,
|
||||
|
||||
@@ -665,6 +665,108 @@ it("selects analyzer-produced boundaries by their method field", () => {
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it.each(["Number", "JSON.parse"])(
|
||||
"connects a resolved %s boundary to its assigned result and later read",
|
||||
(method) => {
|
||||
const graph = graphFor(
|
||||
`function parse(raw) { const n = ${method}(raw); return n; }`,
|
||||
);
|
||||
const boundary = graph.nodes.find(
|
||||
(node) => node.kind === "boundary" && node.label === method,
|
||||
);
|
||||
const binding = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "n",
|
||||
);
|
||||
const raw = graph.nodes.find(
|
||||
(node) => node.kind === "parameter" && node.label === "raw",
|
||||
);
|
||||
const read = graph.nodes.find((node) =>
|
||||
node.identity.role_key.endsWith("read:n"),
|
||||
);
|
||||
if (!boundary || !binding || !raw || !read)
|
||||
throw new Error("Expected boundary, input, result binding, and read");
|
||||
expect(graph.relations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_node_id: boundary.node_id,
|
||||
target_node_id: binding.node_id,
|
||||
relation: "defines",
|
||||
resolution: "resolved",
|
||||
}),
|
||||
);
|
||||
const query = (
|
||||
nodeId: string,
|
||||
direction: "forward-influence" | "backward-provenance",
|
||||
) =>
|
||||
queryJavaScriptSemanticGraph(
|
||||
graph,
|
||||
javaScriptSemanticQueryInputSchema.parse({
|
||||
seed: { kind: "semantic-node", node_id: nodeId },
|
||||
direction,
|
||||
}),
|
||||
);
|
||||
expect(query(boundary.node_id, "forward-influence").nodes).toEqual(
|
||||
expect.arrayContaining([binding, read]),
|
||||
);
|
||||
expect(query(read.node_id, "backward-provenance").nodes).toEqual(
|
||||
expect.arrayContaining([boundary, raw]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"function parse(raw) { const n = Number(raw) + 1; return n; }",
|
||||
"function parse(raw) { let n = 1; n += Number(raw); return n; }",
|
||||
"function parse(raw) { let n = 1; n ||= Number(raw); return n; }",
|
||||
])(
|
||||
"does not treat a transformed or conditional boundary as its assigned value: %s",
|
||||
(source) => {
|
||||
const graph = graphFor(source);
|
||||
const boundary = graph.nodes.find((node) => node.kind === "boundary");
|
||||
const binding = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "n",
|
||||
);
|
||||
if (!boundary || !binding) throw new Error("Expected boundary and binding");
|
||||
expect(graph.relations).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_node_id: boundary.node_id,
|
||||
target_node_id: binding.node_id,
|
||||
relation: "defines",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps uncertain parser results as candidates and ignores shadowed coercions", () => {
|
||||
const graph = graphFor(`
|
||||
function run(raw, parser, Number) {
|
||||
const parsed = parser.parse(raw);
|
||||
const shadowed = Number(raw);
|
||||
return parsed;
|
||||
}
|
||||
`);
|
||||
const parser = graph.nodes.find(
|
||||
(node) => node.kind === "boundary" && node.label === "parse",
|
||||
);
|
||||
const parsed = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "parsed",
|
||||
);
|
||||
if (!parser || !parsed)
|
||||
throw new Error("Expected parser boundary and binding");
|
||||
expect(graph.relations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_node_id: parser.node_id,
|
||||
target_node_id: parsed.node_id,
|
||||
relation: "defines",
|
||||
resolution: "candidate",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
graph.nodes.some(
|
||||
(node) => node.kind === "boundary" && node.label === "Number",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
const semanticNodeFor = (roleKey: string) => {
|
||||
const file: JavaScriptArtifactFile = {
|
||||
path: "budget.js",
|
||||
|
||||
@@ -237,6 +237,16 @@ export const projectSemanticBoundaries = (
|
||||
resolution:
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
});
|
||||
// The producer only supplies a scalar result binding when this call is
|
||||
// the assigned value itself, rather than an operand of another expression.
|
||||
if (operation.resultBindingId !== null)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: boundary,
|
||||
target: context.bindingNodes.get(operation.resultBindingId),
|
||||
relation: "defines",
|
||||
resolution:
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
});
|
||||
if (operation.resolution !== "complete")
|
||||
addDataUnknown({
|
||||
context,
|
||||
|
||||
@@ -57,7 +57,7 @@ export const assignedSemanticResultBindings = (
|
||||
if (t.isVariableDeclarator(owner)) {
|
||||
pattern = owner.id;
|
||||
source = owner.init ?? null;
|
||||
} else if (t.isAssignmentExpression(owner)) {
|
||||
} else if (t.isAssignmentExpression(owner, { operator: "=" })) {
|
||||
pattern = owner.left;
|
||||
source = owner.right;
|
||||
}
|
||||
|
||||
@@ -60,3 +60,52 @@ cliTest(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"traces a coercion boundary through its assigned result",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-graph-boundary-result-");
|
||||
await writeFile(
|
||||
join(root, "main.js"),
|
||||
"function parse(raw) { const n = Number(raw); return n; } module.exports = parse;",
|
||||
);
|
||||
const analysis = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--format", "json"],
|
||||
});
|
||||
expect(analysis.exitCode, analysis.stderr).toBe(0);
|
||||
const application = parseEvidence(analysis.json);
|
||||
const graph = javascriptApplicationAnalysisResultSchema.parse(
|
||||
application.normalized_result,
|
||||
).semantic_graph;
|
||||
if (graph === null) throw new Error("Expected semantic graph");
|
||||
const read = graph.nodes.find((node) =>
|
||||
node.identity.role_key.endsWith("read:n"),
|
||||
);
|
||||
if (read === undefined) throw new Error("Expected result read");
|
||||
const query = {
|
||||
seed: { kind: "boundary-field", field: "Number" },
|
||||
direction: "forward-influence",
|
||||
};
|
||||
const request = join(root, "request.json");
|
||||
await writeFile(request, JSON.stringify({ application, query }));
|
||||
const result = await cli.run({
|
||||
arguments: ["trace-javascript-semantics", request, "--format", "json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const trace = javaScriptSemanticTraceResultSchema.parse(
|
||||
parseEvidence(result.json).normalized_result,
|
||||
);
|
||||
expect(trace.nodes).toContainEqual(read);
|
||||
const call = await cli.sharedMcpCall?.();
|
||||
if (call === undefined) throw new Error("Expected shared MCP session");
|
||||
const mcp = await call("trace_javascript_semantics", {
|
||||
application,
|
||||
query,
|
||||
});
|
||||
expect(mcp.isError, JSON.stringify(mcp.content)).not.toBe(true);
|
||||
const mcpTrace = javaScriptSemanticTraceResultSchema.parse(
|
||||
parseEvidence(mcp.structuredContent).normalized_result,
|
||||
);
|
||||
expect(mcpTrace.nodes).toContainEqual(read);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user