Skip to content

Go: a package's type is fragmented into one node per file that declares a method on it #3399

Description

@xiongjianxu

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() {}
$ graphify extract svc/

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.

Activity

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