Skip to content

--emit neo4j: J_CALLS materializes only ~14% of the call graph (edges absent though both endpoint nodes exist) #158

Open

Description

@rahlk

Is your feature request related to a problem? Please describe.

--emit neo4j materializes only a small fraction of the call graph: on daytrader8 the emitted graph has 287 J_CALLS edges, while the analysis.json call_graph from an equivalent level-2 run has ~2077 edges. Edges are missing even when both endpoint :JCallable nodes exist in the graph, so it is not (only) the external-target gating.

Any consumer reading the call graph from Neo4j (e.g. the CLDK SDK's read-only Java backend) sees ~14% of the call edges the in-memory / analysis.json backend reports — callers/callees, class call graphs, and reachability are all substantially incomplete.

Describe the solution you'd like

Not stated in the original issue.

Describe alternatives you've considered

Not stated in the original issue.

Additional context

Environment

  • codeanalyzer-java 2.4.0 (--analysis-level 2, live Bolt push)

Evidence

  • The call graph is deterministic: two --emit json runs gave 2077 and 2076 call_graph edges.
  • The same project --emit neo4j produced 287 J_CALLS edges.
  • Of 1855 fully-resolved edges in analysis.json (both endpoints are app callables present in the symbol table), only ~287 unique have a J_CALLS edge in the graph; ~1523 are absent despite both endpoint :JCallable nodes existing:
# for an absent edge, both nodes resolve:
MATCH (a:JCallable {id:$sid}) RETURN a   // exists
MATCH (b:JCallable {id:$tid}) RETURN b   // exists
MATCH (:JCallable {id:$sid})-[r:J_CALLS]->(:JCallable {id:$tid}) RETURN count(r)  // 0

Example absent edges (both endpoints present as nodes): ...web.websocket.ActionMessage#... -> ...util.Log#error(...), ...web.prims.PingWebSocketJson#... -> ...web.websocket.JsonMessage#....

Likely area

neo4j/GraphProjector.java, projectCallGraph → b.edgeIfBothResolved("J_CALLS", new NodeRef("JSymbol","id",from), new NodeRef("JSymbol","id",to), props) with from/to = type_declaration + "#" + signature. Since both endpoint nodes exist with exactly those ids, edgeIfBothResolved should retain them — so either the resolution key used during projection differs from the node id actually emitted (a normalization mismatch between the call_graph vertices' type_declaration/signature and the symbol-table ids), or the --emit neo4j path is projecting a much smaller call graph than --emit json. Worth confirming whether the two emit paths build the SDG identically.

Activity

  1. added a commit that references this issue on Jun 23, 2026
  2. rahlk commented on Jun 23, 2026

    @rahlk
    CollaboratorAuthor

    Verified fixed in v2.4.1 (PR #159). Rebuilt the fatJar at the v2.4.1 tag and re-ran on daytrader8 (level 2): J_CALLS went from 287 → 1702 edges. The vertexId fix (keying off callable_declaration instead of the call-graph signature, which rewrote <init>/<clinit>) resolves the gating. The small residual vs the analysis.json call graph (~1761) is run-to-run WALA variance between the separate --emit json and --emit neo4j invocations, not the projection. Good to close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions