From e6f064e87a4375ed8a62da5014572b5b70ad7a4c Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Wed, 9 Sep 2026 06:03:00 -0400 Subject: [PATCH] fix(sdg): param_in/param_out carry the bound formal's var in JSON and on J_PARAM_IN/OUT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog declared `var` on J_PARAM_IN and J_PARAM_OUT and the projector emitted both with no properties, so a Cypher predicate on `r.var` went three-valued across every call boundary and silently dropped the path. The variable was already known where the edges are built — each actual binds a named callee formal — but JIdEdge had nowhere to carry it. JIdEdge.var now holds it: the bound parameter's name on param_in, `$ret` on param_out. Summary edges keep the bare shape (Gson omits an unset var rather than writing null). The conformance test now asserts every emitted edge of both types carries every declared property, on all of them rather than some — a partially present property is the same trap one step along. Same defect and same direction as codeanalyzer-python#195. Closes #249 --- .../com/ibm/cldk/neo4j/V2GraphProjector.java | 14 +++++++++-- .../java/com/ibm/cldk/schema/JIdEdge.java | 11 ++++++-- .../dataflow/SdgVertices.java | 12 +++++++-- .../neo4j/V2Neo4jSchemaConformanceTest.java | 25 +++++++++++++++++++ .../dataflow/SdgVerticesTest.java | 2 ++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ibm/cldk/neo4j/V2GraphProjector.java b/src/main/java/com/ibm/cldk/neo4j/V2GraphProjector.java index f019b3a6..c8c06452 100644 --- a/src/main/java/com/ibm/cldk/neo4j/V2GraphProjector.java +++ b/src/main/java/com/ibm/cldk/neo4j/V2GraphProjector.java @@ -147,14 +147,14 @@ public static GraphRows project(Analysis analysis, String appName) { for (JIdEdge e : analysis.getApplication().getParamIn()) { b.edgeIfBothResolved("J_PARAM_IN", new NodeRef("JBodyNode", "id", e.getSrc()), - new NodeRef("JBodyNode", "id", e.getDst()), RowBuilder.props()); + new NodeRef("JBodyNode", "id", e.getDst()), paramEdgeProps(e)); } } if (analysis.getApplication().getParamOut() != null) { for (JIdEdge e : analysis.getApplication().getParamOut()) { b.edgeIfBothResolved("J_PARAM_OUT", new NodeRef("JBodyNode", "id", e.getSrc()), - new NodeRef("JBodyNode", "id", e.getDst()), RowBuilder.props()); + new NodeRef("JBodyNode", "id", e.getDst()), paramEdgeProps(e)); } } @@ -731,4 +731,14 @@ private static Map mapOf(String k, Object v) { m.put(k, v); return m; } + + /** + * {@code var} on a param edge: the callee-side formal's variable, which the catalog declares + * and every {@code param_in}/{@code param_out} edge carries (codeanalyzer-python#195). + */ + private static Map paramEdgeProps(JIdEdge e) { + Map p = RowBuilder.props(); + p.put("var", e.getVar()); + return RowBuilder.prune(p); + } } diff --git a/src/main/java/com/ibm/cldk/schema/JIdEdge.java b/src/main/java/com/ibm/cldk/schema/JIdEdge.java index a0fdaf0f..239de7eb 100644 --- a/src/main/java/com/ibm/cldk/schema/JIdEdge.java +++ b/src/main/java/com/ibm/cldk/schema/JIdEdge.java @@ -3,11 +3,18 @@ import lombok.Data; /** - * An id-to-id edge with no payload — the shape of {@code param_in}/{@code param_out} (application - * scope, global-ordinal endpoints) and {@code summary} (callable scope, local endpoints). + * An id-to-id edge — the shape of {@code param_in}/{@code param_out} (application scope, + * global-ordinal endpoints) and {@code summary} (callable scope, local endpoints). + * + *

{@code var} is the callee-side formal's variable on a {@code param_in}/{@code param_out} + * edge — the parameter name, or {@code $ret} for the return port — set on every such edge. The + * Neo4j catalog declared it on {@code J_PARAM_IN}/{@code J_PARAM_OUT} while nothing wrote it, so + * a Cypher predicate on {@code r.var} went three-valued across every call boundary + * (codeanalyzer-python#195). Absent (never null-serialized) on {@code summary} edges. */ @Data public class JIdEdge { private String src; private String dst; + private String var; } diff --git a/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVertices.java b/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVertices.java index 1227400a..a1ef5746 100644 --- a/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVertices.java +++ b/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVertices.java @@ -118,13 +118,14 @@ private static void addActualsAndEdges( for (int i = 0; i < nArgs; i++) { int formalIndex = Math.min(i, calleeParams - 1); paramIn.add(edge( - global(c, local + "/actual_in:" + i), global(callee, "@formal_in:" + formalIndex))); + global(c, local + "/actual_in:" + i), global(callee, "@formal_in:" + formalIndex), + callee.getParameters().get(formalIndex).getName())); } } // Recomputed rather than read off callee.getBody(): traversal order is not call order, so // the callee's own addFormals may not have run yet when the caller's site is processed. if (hasActualOut && returnsValue(callee)) { - paramOut.add(edge(global(callee, "@formal_out"), global(c, local + "/actual_out"))); + paramOut.add(edge(global(callee, "@formal_out"), global(c, local + "/actual_out"), "$ret")); } } } @@ -143,6 +144,13 @@ private static JBodyNode vertex(String kind, String of, String parent) { return n; } + /** A param edge names the callee-side formal it binds: the parameter, or {@code $ret}. */ + private static JIdEdge edge(String src, String dst, String var) { + JIdEdge e = edge(src, dst); + e.setVar(var); + return e; + } + private static JIdEdge edge(String src, String dst) { JIdEdge e = new JIdEdge(); e.setSrc(src); diff --git a/src/test/java/com/ibm/cldk/neo4j/V2Neo4jSchemaConformanceTest.java b/src/test/java/com/ibm/cldk/neo4j/V2Neo4jSchemaConformanceTest.java index a9b94333..0512d62c 100644 --- a/src/test/java/com/ibm/cldk/neo4j/V2Neo4jSchemaConformanceTest.java +++ b/src/test/java/com/ibm/cldk/neo4j/V2Neo4jSchemaConformanceTest.java @@ -40,6 +40,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -165,6 +166,30 @@ public void everyEmittedNodeLabelAndPropertyIsDeclared() { } } + /** + * codeanalyzer-python#195, same defect here: the catalog declared {@code var} on + * J_PARAM_IN/J_PARAM_OUT and the projection wrote no properties, so a predicate on + * {@code r.var} went three-valued across every call boundary. Every emitted edge of those + * types carries every declared property — on all of them, not some. + */ + @Test + public void paramEdgesCarryEveryDeclaredProperty() { + for (String rel : Arrays.asList("J_PARAM_IN", "J_PARAM_OUT")) { + int seen = 0; + for (EdgeRow e : rows.edges) { + if (!e.type.equals(rel)) { + continue; + } + seen++; + for (String key : REL_BY_TYPE.get(rel).properties.keySet()) { + assertTrue(e.props.containsKey(key) && e.props.get(key) != null, + rel + " " + e.from.value + " -> " + e.to.value + " lacks " + key); + } + } + assertTrue(seen > 0, "precondition: the L4 fixture must emit " + rel); + } + } + @Test public void everyEmittedRelationshipIsDeclared() { assertTrue(rows.edges.size() > 0, "fixture produced no edges"); diff --git a/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVerticesTest.java b/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVerticesTest.java index b706eba8..40628399 100644 --- a/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVerticesTest.java +++ b/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/SdgVerticesTest.java @@ -82,9 +82,11 @@ void buildsFormalActualVerticesAndParamEdges() { JIdEdge in = r.paramIn.get(0); assertEquals(CanId.ordinalId(A_ID, "3:16/actual_in:0"), in.getSrc()); assertEquals(CanId.ordinalId(B_ID, "formal_in:0"), in.getDst()); + assertEquals("y", in.getVar(), "param_in names the callee formal it binds (#195)"); assertEquals(1, r.paramOut.size()); assertEquals(CanId.ordinalId(B_ID, "formal_out"), r.paramOut.get(0).getSrc()); assertEquals(CanId.ordinalId(A_ID, "3:16/actual_out"), r.paramOut.get(0).getDst()); + assertEquals("$ret", r.paramOut.get(0).getVar(), "param_out names the return port"); } @Test