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
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