Repository navigation
fix(go): a call on an embedded struct's method does not reach the embedder's method of that name - #2430
Merged
Conversation
…edder's method of that name
Go has no inheritance. A struct that embeds another (`type Engine struct
{ RouterGroup }`) has an `extends` edge to it since #2397, and the
interface-dispatch bridge read every `extends` edge as an override, so it
linked RouterGroup.Use to Engine.Use. A call on a *RouterGroup only ever
runs RouterGroup's own Use, so a flow or impact walk through it went on
into Engine.Use and the code only the engine runs.
The bridge now skips a Go type's supertype edge unless it points at an
interface: only a call through an interface dispatches. Structs that embed
an interface keep their links, and so do the go-implements edges, so a call
through IRoutes still reaches both RouterGroup.Use and Engine.Use. Other
languages are unchanged.
Removed on gin / prometheus / etcd: 1 / 112 / 60 interface-impl edges, all
from a struct's method to a struct embedding it; nothing else in the graph
changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
Merged
colbymchenry
added a commit
that referenced
this pull request
Oct 21, 2026
…ns put in it (#2745) A Go struct can hold a function in a field and call it through the field. Prometheus' service discovery runs every provider that way: refresh.Discovery calls d.refreshf(ctx), NewDiscovery copies refreshf from Options.RefreshF, and each provider passes its own method (RefreshF: d.refresh). Nothing answered d.refreshf, so a flow from Discovery.Run stopped at the field; #2430 rightly dropped the interface-impl edges that had stood in for it. goFuncFieldEdges, a Go-only synthesis pass, links a call x.F(...) through a func field of a struct to each function or method value put in that field: by a composite literal (keyed or positional, found through its instantiates edge), by an assignment x.F = v, or by one level of field-to-field copy (refreshf: opts.RefreshF). A value is what the resolver already linked it to, else x's method m for x.m, an interface's method for an interface value. Receivers are typed through the resolver's own Go scope reader (goLocalDeclarationAt over goLocalDecl, #2706), or by a local's composite literal, new(T), type assertion or resolved call result. Channels are keyed by package, struct and field, so a generic field name never crosses between unrelated structs; one package's test wiring links no other package's test; an edge cites a function's production wiring; generated files are not read. Edges are calls, provenance heuristic, synthesizedBy go-func-field, with field, via (package-qualified when the copied field is another package's) and registeredAt; explore, the node trail and ContextBuilder read them as "func field `Discovery.refreshf` (set from `Options.RefreshF`)". The pass sits after the Go framework passes, with a pool cost hint, and every Go file now counts as a synthesis input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Go has no inheritance.
type Engine struct { RouterGroup }embedsRouterGroup, and since #2397 that embedding is anextendsedge. The interface-dispatch bridge (interfaceOverrideEdges, theinterface-implpass) read everyextendsedge as an override. It linked each embedded struct's method to the embedder's method of the same name, so gin gotRouterGroup.Use → Engine.Use. A call on a*RouterGrouponly ever runsRouterGroup.Use, so a flow or impact walk through it wrongly went on intoEngine.Useand the code only the engine runs.The bridge now skips a Go type's supertype edge unless it points at an interface. In Go only a call through an interface dispatches. These are unchanged:
type → interfaceedges, so a call throughIRoutes.Usestill reaches bothRouterGroup.UseandEngine.Use;limitAppender { Appender });The change is one guard in
src/resolution/callback-synthesizer.ts.Validation
Regression test:
__tests__/go-embedding-no-override.test.ts, 5 tests.RouterGroup.Use), a defined type (HandlersChain.Last), a pointer embedding (*Mutex), a cross-package embedding (api.UnimplementedAuthServer), and the callee, caller and impact walks.Before/after graph diff: gin, prometheus and etcd, indexed with main ed199e6 and with this branch. Both arms share the same kernel, and only
callback-synthesizer.jsdiffers. Every removed edge was triaged. An earlier round on main 2f2afea, before #2417 merged, removed exactly the same edges.The triage checks each removed edge for all of these:
callsedge from theinterface-implpass, with nopromotedInto;extendsedge straight to that struct.All 173 edges have this shape. No removed pair survives under another synthesizer's label: the gRPC stub bridge does not pick up etcd's
UnimplementedAuthServer.Authenticate → mockAuthServer.Authenticate. After the fix, every Gointerface-impledge starts at an interface method (179 / 3345 / 2565). Examples of removed edges:RouterGroup.Use → Engine.Use,EC2SDConfig.NewDiscoverer → SDConfig.NewDiscoverer,Mutex.Lock → lockerMutex.Lock,maintenanceServer.Defragment → authMaintenanceServer.Defragment,batchTx.Unlock → batchTxBuffered.Unlock.codegraph_explore on gin (
RouterGroup.Use Engine.Use rebuild404Handlers):Use (routergroup.go:89) ↓ dynamic: interface → impl @gin.go:363 → Use (gin.go:363) ↓ calls → rebuild404Handlers. A router group'sUsenever rebuilds the engine's 404 handlers.Engine.Use's blast radius drops from 6 callers to 5.codegraph_node RouterGroup.Useno longer listsUse (gin.go:363) [dynamic: interface → impl]among its calls.Trade-off: prometheus's refresh flow goes silent
28 of prometheus's 112 removed edges are
refresh.Discovery.refresh → <provider>.refresh, one for each service discovery that embeds*refresh.Discovery. The link is false as dispatch through embedding. But the call does happen at runtime, through a function field:refresh.Discovery.refreshcallsd.refreshf(ctx);refresh.NewDiscoverycopiesopts.RefreshFintorefreshf;refresh.Options{RefreshF: d.refresh}(discovery/aws/ec2.go:221and so on).The false edges approximated that flow. On main, explore for
Discovery.Run EC2Discovery.refreshprintsRun → refresh ↓ dynamic: interface → impl @discovery/aws/ec2.go:307 → refresh (ec2.go). That is the right path for the wrong reason, with the wrong wiring site. AddingDiscovery.refreshto the query makes it pick uyuni'srefreshinstead of EC2's. With this change, no Flow is printed: the path stops atd.refreshf(ctx). Nothing in the synthesizer bridges a Go function-typed field today;fieldChannelEdgesmatches JSthis.patterns only. A dedicated bridge would restore the flow with the real wiring site (RefreshF: d.refresh). It is filed as a follow-up and kept out of this PR.20 of the 28 targets have a value reference into them (
RefreshF: d.refresh). The other 8 are wired through arefresherinterface value (RefreshF: r.refreshin hetzner, scaleway, ovhcloud and stackit) or ionos'sd.refresh. No target of any other removed edge in the three repos has a value reference, so none of gin's or etcd's removed edges, and no other prometheus one, carries a call like this.Related, not changed here
router := New(); router.Use(…). All 56 such calls resolve toRouterGroup.Useby the receiver-word guess (instance-method, 0.65); at runtime they runEngine.Use. Before this change, the false edge then led them intoEngine.Use. The call-result receiver typing in open PR fix(go): type receivers from package-qualified parameters and call results; no guess for outside types #1954 may cover this.refresher {refresh}in six discovery packages, each matched to about 37 providers), feeding 264 dispatch edges. Filed as a follow-up. etcd's 27 aremustEmbedUnimplemented…gRPC interfaces, which a server in another package does satisfy by embedding the stub, so the rule must check where the method is declared.Overlap
This branch is on top of #2414 (promoted-method dispatch), #2419 (defined types as implementers) and #2417 (
type A = Baliases), all merged while it was in progress. The new guard sits inside #2419's reworked loop:→ interfaceedges, so it keeps all of them.promotesalready required an interface base.type A = Bis indexed and links to the type it names #2417 can be an embedding target, and the guard skips it like any non-interface type. An alias of an interface owns no methods, so it linked nothing before either. Embedding an alias of a struct is embedding that struct.Tests
npx tsc --noEmit: clean.go-type-aliasandcpp-library-type-receiverfrom the two PRs that merged last;go-implements-embedding,go-promoted-dispatch,go-implements-defined-types,go-interface-embeddingandgo-type-position-kinds;ui-flow-api,resolutionandframeworks-integration.EBUSYlocks while removing temp dirs, and 1 was the mpeg-ts timing bound.sync.test.tspasses 58/58.daemon-pid-reuse(EBUSYat teardown),function-ref's Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case (its own 60 s timeout) andmpeg-ts-not-typescript(2376 ms against a 2000 ms bound). All three fail the same way on main ed199e6's own checkout; mpeg-ts is worse there, with 5822 ms and two 30 s timeouts. They come from the box, not this change.🤖 Generated with Claude Code