Summary
A Go type whose methods live in other files of the same package becomes several nodes, one
per file, each owning only the methods declared there. The declaring file's node owns none of
them. Every downstream single-definition guard then reads the set as an ambiguity and bails, so
an idiomatic multi-file package loses type-level resolution.
C/C++/ObjC hit the same id-collision between a header declaration and its impl and avoid this
through _merge_decl_def_classes (graphify/extractors/resolution.py), which folds the
collision before _disambiguate_colliding_node_ids splits it. Go has no equivalent.
Reproducer
svc/a.go package svc; type Server struct{}; func Run(srv *Server) { srv.Close() }
svc/b.go package svc; func (s *Server) Close() {}
svc/c.go package svc; func (s *Server) Save() {}
Actual — three Server nodes, and the methods hang off two of them:
nodes labelled Server:
..._svc_a_go_svc_server | a.go
..._svc_b_go_svc_server | b.go
..._svc_c_go_svc_server | c.go
edges:
a.go -> Server | contains
Run() -> Server | references (parameter_type)
Server -> .Close() | method <- the b.go node
Server -> .Save() | method <- the c.go node
Expected — one Server, sourced at its declaration, owning both methods:
svc_server | a.go
a.go -> Server | contains
Run() -> Server | references (parameter_type)
Server -> .Close() | method
Server -> .Save() | method
Why
extract_go mints the receiver's type node in every file that declares a method on it
(graphify/extractors/go.py, the method_declaration branch), keyed
_make_id(pkg_scope, receiver_type) where pkg_scope is the package directory's name. All those
nodes therefore share one id and differ only in source_file — which is exactly the input
_disambiguate_colliding_node_ids is built to split, so it salts each one with its own path.
Impact
references to the type land on whichever fragment the referring file produced, so a type's
incoming uses and its methods are no longer reachable from one node.
- The id is salted with a file path, so it is neither the canonical
{pkg}_{type} form nor
stable across machines for the affected types.
- Every resolver that requires exactly one declaration of a name (member-call resolution, the
Go type-reference rewire) bails, which is silent under-reporting rather than a visible error.
Splitting one Go type into two files is not an edge case; it is how any package larger than a
single file is written.
Fix
Fold the id-collision before disambiguation, mirroring _merge_decl_def_classes. The nodes
already share an id, so no edge re-pointing is needed — only the redundant duplicates are
dropped and the declaring file's node survives.
Guards against a false merge: all members of the collision must be .go files in one
directory (the id folds in only the directory's name, so a/svc and b/svc collide while
being different packages), and a _test.go member stops the fold, since its package clause may
be the separate external svc_test package declaring a type of the same name — extract_go
does not parse the package clause, so same-directory is not evidence of same package there.
PR follows.
Summary
A Go type whose methods live in other files of the same package becomes several nodes, one
per file, each owning only the methods declared there. The declaring file's node owns none of
them. Every downstream single-definition guard then reads the set as an ambiguity and bails, so
an idiomatic multi-file package loses type-level resolution.
C/C++/ObjC hit the same id-collision between a header declaration and its impl and avoid this
through
_merge_decl_def_classes(graphify/extractors/resolution.py), which folds thecollision before
_disambiguate_colliding_node_idssplits it. Go has no equivalent.Reproducer
Actual — three
Servernodes, and the methods hang off two of them:Expected — one
Server, sourced at its declaration, owning both methods:Why
extract_gomints the receiver's type node in every file that declares a method on it(
graphify/extractors/go.py, themethod_declarationbranch), keyed_make_id(pkg_scope, receiver_type)wherepkg_scopeis the package directory's name. All thosenodes therefore share one id and differ only in
source_file— which is exactly the input_disambiguate_colliding_node_idsis built to split, so it salts each one with its own path.Impact
referencesto the type land on whichever fragment the referring file produced, so a type'sincoming uses and its methods are no longer reachable from one node.
{pkg}_{type}form norstable across machines for the affected types.
Go type-reference rewire) bails, which is silent under-reporting rather than a visible error.
Splitting one Go type into two files is not an edge case; it is how any package larger than a
single file is written.
Fix
Fold the id-collision before disambiguation, mirroring
_merge_decl_def_classes. The nodesalready share an id, so no edge re-pointing is needed — only the redundant duplicates are
dropped and the declaring file's node survives.
Guards against a false merge: all members of the collision must be
.gofiles in onedirectory (the id folds in only the directory's name, so
a/svcandb/svccollide whilebeing different packages), and a
_test.gomember stops the fold, since its package clause maybe the separate external
svc_testpackage declaring a type of the same name —extract_godoes not parse the package clause, so same-directory is not evidence of same package there.
PR follows.