6868 VIA ,
6969 GoAnalysisBackend ,
7070)
71- from cldk .analysis .go .ids import body_node_id , callable_match_names , external_signature , go_value_names
71+ from cldk .analysis .go .ids import body_node_id , callable_match_names , external_signature , go_value_names , pick_declaration
7272from cldk .models .go import (
7373 GoAnalysis ,
7474 GoApplication ,
@@ -526,7 +526,7 @@ def _slice_node(self, ref: str) -> SliceNode | None:
526526
527527 # -----[ addressing ]-----
528528 @cached_property
529- def _candidates (self ) -> Tuple [List [CallableCandidate ], Dict [str , Tuple [object , str | None , str ]], FrozenSet [ str ]]:
529+ def _candidates (self ) -> Tuple [List [CallableCandidate ], Dict [str , List [ Tuple [object , str | None , str ]]]]:
530530 """``(candidates, {signature: owner}, colliding signatures)`` for :meth:`resolve_callable`.
531531
532532 **The library stand-ins are in the domain**, and they have to be: spec §7's own worked
@@ -542,28 +542,33 @@ def _candidates(self) -> Tuple[List[CallableCandidate], Dict[str, Tuple[object,
542542 than matching one through a path derivation that would be meaningless here (the same
543543 distinction Java's J-2 records).
544544
545- Two callables sharing a signature collapse into one entry, and the resolver would then hand
546- back whichever won with no signal — so the collisions are recorded and a name resolving onto
547- one raises. Checked on the *answer* rather than eagerly, so a duplicate elsewhere does not
548- break every unrelated resolution.
545+ **One candidate per declaration, not per signature.** A Go signature is not a unique
546+ address: several files in one package may each declare their own ``init()``, so
547+ ``example.com/x/initclosure.init`` names two different callables with different bodies (3
548+ such collisions on the analyzer's own corpus). Keying the candidate list by signature
549+ collapsed those into one entry, which hid the ambiguity from the shared policy — so it
550+ could neither report it (the Neo4j backend, whose rows are per declaration, raised
551+ ``AmbiguousName`` where this raised a bare ``ValueError``) nor let ``in_module=`` narrow it.
552+ Emitting one candidate per declaration is what makes the two backends hand the policy the
553+ same domain, which is the only way they can agree on both the answer and the failure.
554+
555+ ``declarations`` therefore maps a signature to *every* callable carrying it, and
556+ :func:`~cldk.analysis.go.ids.pick_declaration` re-applies the caller's keywords to choose
557+ among them once the policy has resolved the signature.
549558 """
550- found : Dict [str , Tuple [object , str | None , str ]] = {}
551- collisions : set = set ()
559+ declarations : Dict [str , List [Tuple [object , str | None , str ]]] = {}
552560 for c , tid , path in self ._iter_callables ():
553- if c .signature in found :
554- collisions .add (c .signature )
555- found [c .signature ] = (c , tid , path )
561+ declarations .setdefault (c .signature , []).append ((c , tid , path ))
556562 cands = [
557563 CallableCandidate (sig , tid , path , match_names = callable_match_names (sig ), module_names = (pkg ,) if (pkg := self ._package_of (path )) else ())
558- for sig , (_c , tid , path ) in found .items ()
564+ for sig , entries in declarations .items ()
565+ for _c , tid , path in entries
559566 ]
560567 for sym in self .application .external_symbols .values ():
561568 sig = external_signature (sym .module , sym .signature , sym .name )
562- if sig in found :
563- collisions .add (sig )
564- found [sig ] = (sym , None , "" )
569+ declarations .setdefault (sig , []).append ((sym , None , "" ))
565570 cands .append (CallableCandidate (sig , None , "" , match_names = callable_match_names (sig ), module_names = ()))
566- return cands , found , frozenset ( collisions )
571+ return cands , declarations
567572
568573 def _package_of (self , path : str ) -> str | None :
569574 """The ``package`` clause of the module at ``path`` — what ``in_module=`` matches a Go
@@ -585,11 +590,14 @@ def resolve_callable(self, name: str, *, in_class: str | None = None, in_module:
585590 "both backends resolve over the same set". The Go receiver spellings come from
586591 :func:`~cldk.analysis.go.ids.callable_match_names`, which is shared with the Neo4j backend.
587592 """
588- cands , found , collisions = self ._candidates
593+ cands , declarations = self ._candidates
589594 sig = resolve_callable_signature (name , cands , in_class = in_class , in_module = in_module , by_full_name = "more of the import path" )
590- if sig in collisions :
591- raise ValueError (f"{ sig !r} is carried by more than one analysed callable; neither can be addressed unambiguously" )
592- c , _tid , path = found [sig ]
595+ entries = declarations [sig ]
596+ # A signature can name several declarations (same-package multi-file ``init()``), so the
597+ # keywords are re-applied to choose among them -- see ``pick_declaration`` for why the
598+ # policy's own answer is one step short of an address here.
599+ chosen = pick_declaration (sig , [(path , tid ) for _c , tid , path in entries ], in_class = in_class , in_module = in_module )
600+ c , _tid , path = next ((obj , tid , p ) for obj , tid , p in entries if p == chosen )
593601 if not c .id :
594602 raise KeyError (f"{ sig !r} carries no usable address (id={ c .id !r} )" )
595603 # A stand-in has no file and no span, so it is described as an external rather than given a
@@ -629,17 +637,31 @@ def resolve_value(self, name: str, *, within: str) -> SliceNode:
629637 return SliceNode (file = owner .file , line = owner .line , callable = owner .callable , kind = kind , name = leaf , defined_in = defined_in , source = None , ref = body_node_id (c .id , key ))
630638
631639 # -----[ per-callable graphs ]-----
632- def _graphs_of (self , name : str , in_class : str | None , page_size : int ) -> GoCallable :
633- """The callable ``name`` resolves to , once this backend is deep enough to have dataflow.
640+ def _graphs_of (self , name : str , in_class : str | None , page_size : int ) -> Tuple [ str , GoCallable | None ] :
641+ """``(signature, callable)`` for ``name``, once this backend is deep enough to have dataflow.
634642
635643 ``page_size`` is validated **first**, before the level guard and before resolution, so a
636644 malformed argument is a ``ValueError`` before anything else — the order the Neo4j backend
637645 applies too, so the two cannot answer the same bad call with different exceptions.
646+
647+ **The callable is ``None`` for a library stand-in**, which is a real resolution target here
648+ (spec §7's own example, ``database/sql.DB.Query``, is one) and has no intraprocedural flow
649+ to report: the analyzer synthesizes only ``formal_in``/``formal_out`` ports for it, so
650+ :class:`~cldk.models.go.GoExternalSymbol` carries no ``cfg``/``cdg``/``ddg`` field at all.
651+ Returning ``None`` is what lets the three accessors answer with an **empty page**, which is
652+ both the honest answer — the pass ran and a stand-in has no statements — and the answer the
653+ Neo4j backend already gives, since its Cypher simply matches no rows on an external node.
654+ Reading the field blindly raised ``AttributeError`` instead, which was neither.
655+
656+ This is **not** the D7 case the level gate guards: there, empty would be indistinguishable
657+ from "no dependence" because the pass never ran. Here the pass ran and the absence is a
658+ fact about what a stand-in *is*.
638659 """
639660 check_page_size (page_size )
640661 self ._require_dataflow ()
641662 node = self .resolve_callable (name , in_class = in_class )
642- return self ._dataflow_owners [node .ref ][0 ]
663+ owner = self ._dataflow_owners [node .ref ][0 ]
664+ return node .callable , owner if isinstance (owner , GoCallable ) else None
643665
644666 def get_cfg (self , callable : str , * , in_class : str | None = None , page_size : int = DEFAULT_PAGE_SIZE , cursor : str | None = None ) -> EdgePage [GoCfgEdge ]:
645667 """One page of control flow within one callable (see :meth:`GoAnalysisBackend.get_cfg`).
@@ -650,21 +672,21 @@ def get_cfg(self, callable: str, *, in_class: str | None = None, page_size: int
650672 the one the Neo4j backend would return. The join happens *before* the sort, because the
651673 order is over the ids a caller sees, not over the local keys.
652674 """
653- c = self ._graphs_of (callable , in_class , page_size )
654- edges = [GoCfgEdge (src = body_node_id (c .id , e .src ), dst = body_node_id (c .id , e .dst ), kind = e .kind ) for e in c .cfg or []]
655- return edge_page (GoCfgEdge , c . signature , edges , CFG_ORDER , page_size , cursor )
675+ sig , c = self ._graphs_of (callable , in_class , page_size )
676+ edges = [] if c is None else [ GoCfgEdge (src = body_node_id (c .id , e .src ), dst = body_node_id (c .id , e .dst ), kind = e .kind ) for e in c .cfg or []]
677+ return edge_page (GoCfgEdge , sig , edges , CFG_ORDER , page_size , cursor )
656678
657679 def get_cdg (self , callable : str , * , in_class : str | None = None , page_size : int = DEFAULT_PAGE_SIZE , cursor : str | None = None ) -> EdgePage [GoCdgEdge ]:
658680 """One page of control dependence within one callable (see :meth:`GoAnalysisBackend.get_cdg`)."""
659- c = self ._graphs_of (callable , in_class , page_size )
660- edges = [GoCdgEdge (src = body_node_id (c .id , e .src ), dst = body_node_id (c .id , e .dst )) for e in c .cdg or []]
661- return edge_page (GoCdgEdge , c . signature , edges , CDG_ORDER , page_size , cursor )
681+ sig , c = self ._graphs_of (callable , in_class , page_size )
682+ edges = [] if c is None else [ GoCdgEdge (src = body_node_id (c .id , e .src ), dst = body_node_id (c .id , e .dst )) for e in c .cdg or []]
683+ return edge_page (GoCdgEdge , sig , edges , CDG_ORDER , page_size , cursor )
662684
663685 def get_ddg (self , callable : str , * , in_class : str | None = None , page_size : int = DEFAULT_PAGE_SIZE , cursor : str | None = None ) -> EdgePage [GoDdgEdge ]:
664686 """One page of data dependence within one callable (see :meth:`GoAnalysisBackend.get_ddg`)."""
665- c = self ._graphs_of (callable , in_class , page_size )
666- edges = [GoDdgEdge (src = body_node_id (c .id , e .src ), dst = body_node_id (c .id , e .dst ), var = e .var , prov = list (e .prov or [])) for e in c .ddg or []]
667- return edge_page (GoDdgEdge , c . signature , edges , DDG_ORDER , page_size , cursor )
687+ sig , c = self ._graphs_of (callable , in_class , page_size )
688+ edges = [] if c is None else [ GoDdgEdge (src = body_node_id (c .id , e .src ), dst = body_node_id (c .id , e .dst ), var = e .var , prov = list (e .prov or [])) for e in c .ddg or []]
689+ return edge_page (GoDdgEdge , sig , edges , DDG_ORDER , page_size , cursor )
668690
669691 # -----[ slicing and reachability ]-----
670692 def _reach (self , ref : str , direction : str , depth : int | None ) -> set :
0 commit comments