Summary
TSNeo4jBackend.get_call_graph() does not add the synthesized anonymous-callback nodes, so the two TypeScript backends are no longer behaviorally interchangeable for the call graph. The in-memory TSCodeanalyzer.get_call_graph() adds a node per synthesized callable with synthesized=True; the Neo4j backend does not, so graph.nodes[sig].get("synthesized") returns True on one backend and None on the other for the same signature.
Found while reviewing the tsc_only / synthesized-callable work on fix/issue-174 (#174), which introduced TSSynthesizedCallable and wired it through both backends.
Where
In-memory backend adds the nodes — cldk/analysis/typescript/codeanalyzer/codeanalyzer.py:323-325:
# Synthesized anonymous-callback nodes so Jelly's anonymous edges don't dangle.
for sig, syn in self.application.synthesized_callables.items():
graph.add_node(sig, external=False, synthesized=True, name=syn.name, path=syn.path)
Neo4j backend omits the matching loop — cldk/analysis/typescript/neo4j/neo4j_backend.py:411-434. get_call_graph() adds only internal Callable nodes (via _all_callable_props()) and external phantom nodes (via get_external_symbols()), then runs the CALLS edge query. The get_synthesized_callables() helper already exists on this class (neo4j_backend.py:267-274) and is already used by get_application() (line 242) — it's just not used in get_call_graph().
Why it matters
The TSAnalysisBackend ABC and the TSNeo4jBackend docstring both promise the backends are drop-in interchangeable. When a CALLS edge targets a synthesized signature, nx.DiGraph.add_edge auto-creates the endpoint as a bare node with no attributes, so:
TSCodeanalyzer: graph.nodes[sig] → {external: False, synthesized: True, name: ..., path: ...}
TSNeo4jBackend: graph.nodes[sig] → {}
Any consumer iterating graph.nodes(data=True) and filtering on attrs.get("synthesized") silently gets different answers per backend.
(Note: this assumes :AnonymousCallable nodes are also labeled :Symbol so the MATCH (s:Symbol)-[:CALLS]->(t:Symbol) edge query reaches them. If they are not :Symbol, the edges to synthesized callables are dropped entirely on Neo4j and the signatures show up as orphan nodes on codeanalyzer — worth confirming against the emitted graph schema either way.)
Fix
Add the same loop to TSNeo4jBackend.get_call_graph() before the edge query, mirroring the in-memory backend:
for sig, syn in self.get_synthesized_callables().items():
graph.add_node(sig, external=False, synthesized=True, name=syn.name, path=syn.path)
Summary
TSNeo4jBackend.get_call_graph()does not add the synthesized anonymous-callback nodes, so the two TypeScript backends are no longer behaviorally interchangeable for the call graph. The in-memoryTSCodeanalyzer.get_call_graph()adds a node per synthesized callable withsynthesized=True; the Neo4j backend does not, sograph.nodes[sig].get("synthesized")returnsTrueon one backend andNoneon the other for the same signature.Found while reviewing the
tsc_only/ synthesized-callable work onfix/issue-174(#174), which introducedTSSynthesizedCallableand wired it through both backends.Where
In-memory backend adds the nodes —
cldk/analysis/typescript/codeanalyzer/codeanalyzer.py:323-325:Neo4j backend omits the matching loop —
cldk/analysis/typescript/neo4j/neo4j_backend.py:411-434.get_call_graph()adds only internalCallablenodes (via_all_callable_props()) and external phantom nodes (viaget_external_symbols()), then runs theCALLSedge query. Theget_synthesized_callables()helper already exists on this class (neo4j_backend.py:267-274) and is already used byget_application()(line 242) — it's just not used inget_call_graph().Why it matters
The
TSAnalysisBackendABC and theTSNeo4jBackenddocstring both promise the backends are drop-in interchangeable. When aCALLSedge targets a synthesized signature,nx.DiGraph.add_edgeauto-creates the endpoint as a bare node with no attributes, so:TSCodeanalyzer:graph.nodes[sig]→{external: False, synthesized: True, name: ..., path: ...}TSNeo4jBackend:graph.nodes[sig]→{}Any consumer iterating
graph.nodes(data=True)and filtering onattrs.get("synthesized")silently gets different answers per backend.(Note: this assumes
:AnonymousCallablenodes are also labeled:Symbolso theMATCH (s:Symbol)-[:CALLS]->(t:Symbol)edge query reaches them. If they are not:Symbol, the edges to synthesized callables are dropped entirely on Neo4j and the signatures show up as orphan nodes on codeanalyzer — worth confirming against the emitted graph schema either way.)Fix
Add the same loop to
TSNeo4jBackend.get_call_graph()before the edge query, mirroring the in-memory backend: