Repository navigation
fix(go): a call through an interface reaches a method the struct gets by embedding - #2414
Merged
Merged
Conversation
… by embedding Since goImplementsEdges counts promoted methods, a struct can satisfy an interface with methods its embedded types declare. The interface-dispatch bridge linked each interface method only to a method the struct declares itself, so a call through such a method reached nothing when the embedded type does not implement the interface on its own: prometheus's scraper.Report never reached Target.Report through targetScraper. For a Go struct's synthesized implements edge, each interface method the struct does not declare now links to the method Go's selector picks: the shallowest embedded type that has it, nothing when two occurrences tie at that depth (one type reached along two paths included), and nothing when an embedded interface provides it, which is a dynamic call again. These links come after the per-implementer loop, from what the struct's cap has left, so every existing edge is unchanged. They keep interface-impl metadata plus promotedInto, and registeredAt names the struct's embedding; codegraph_explore labels the hop as a promoted method. The embedding test is shared with goImplementsEdges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
This was referenced Oct 7, 2026
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
Since #2402,
goImplementsEdgescounts promoted methods. A structS{ E }satisfies an interfaceIwhen its own methods, plus those promoted from the embeddedE, coverI. The interface-dispatch bridge (interfaceOverrideEdges) still linked each interface method only to a method the struct declares. WhenI.mis promoted fromE, andEdoes not implementIitself, a call throughI.mreached nothing. prometheus'sscraper.Reportnever reachedTarget.ReportthroughtargetScraper{ *Target }, and etcd'sHasher.MemberIDnever reachedEtcdServer.MemberIDthroughhasherAdapter{ *EtcdServer, … }.For a Go struct's synthesized
implementsedge, each interface method the struct doesn't declare now links to the method Go's selector picks:goImplementsEdgesfollows). The predicate for those edges is now shared:isGoEmbedding.type Chain []func()) provide their own methods.Edges and cap:
MAX_CALLBACKS_PER_CHANNEL) has left, so every override edge keeps its slot.provenance: 'heuristic'withsynthesizedBy: 'interface-impl'andvia. They addpromotedInto: <struct>, andregisteredAtis the wiring site: the line of the struct's embedding that brings the method in.codegraph_explorelabels the hopdynamic: interface → method promoted into targetScraper @scrape/scrape.go:752instead of "runs the implementation override".implementsedges are skipped. For a Go struct, a declared edge is an embedded interface, which provides whatever the struct lacks itself.Validation
There are two arms:
mainat 31c3328 (#2402, #2407, #2401 and #2403 included) and this branch. Both use the same kernel, built from main's Rust source, on gin, prometheus and etcd (76d58e3). The nodes are identical, nothing is removed, and the only added edges areinterface-implcalls.interface-implbefore → aftergo-implementsEvery added edge was triaged against the source:
scraper.Reportandscraper.offset→Target's, throughtargetScraper, whose comment says it implementsscraper.SeriesSetandChunkSeriesSetNext/Err/Warnings→blockBaseSeriesSet's, throughblockSeriesSetandblockChunkSeriesSet.newBlockSeriesSetreturns astorage.SeriesSet, andNewBlockChunkSeriesSetastorage.ChunkSeriesSet.TSDBAdminStats.CleanTombstones/Delete/Snapshot→tsdb.DB's, throughweb_test.go'sdbAdapter{ *tsdb.DB }, which is passed asLocalStorage. That coversweb/api/v1's interface andweb/api/testhelpers' copy of it, which has the same signatures.go-implementsmatch:genericSeriesSet.Next/Err/Warnings→blockBaseSeriesSet's.genericSeriesSet.At()returnsLabelsandblockSeriesSet.At()returnsSeries.interfaces.Cluster.Close/WaitLeader→EtcdProcessCluster's, throughe2eCluster.Endpoints/WaitLeader→ the integrationCluster's, throughintegrationCluster.Hasher.MemberID/LinearizableReadNotify→EtcdServer's, throughhasherAdapter.version.Server.LinearizableReadNotify, throughserverVersionAdapter.ServerHealth.Alarms→fakeServer.Alarms, through the test'sfakeHealthServer.go-implementsmatch:Member.Start/Stopthroughe2eCluster. ItsClienttakes options, andMember.Client()doesn't.WaitTime.Wait→testutil.RecorderBuffered.Wait(n int), throughmockwait'swaitRecorder.codegraph_explore, before → after (same queries, both arms):scrapeLoop.scrapeAndReport scraper.Report Target.Report. The flow wasscrapeAndReport → report → scraper.Reportand stopped there; the only dispatch link was the test doubletestScraper.Report. Now it continues↓ dynamic: interface → method promoted into targetScraper @scrape/scrape.go:752toTarget.Report (scrape/target.go:276). 88 symbols across 4 files → 75 across 2.API.series SeriesSet.Next blockBaseSeriesSet.Next. There was no flow. Now it readsseries (web/api/v1/api.go:1082) → SeriesSet.Next → blockBaseSeriesSet.Next (tsdb/querier.go:588), throughblockSeriesSet's embedding.corruptionChecker.InitialCheck Hasher.MemberID EtcdServer.MemberID. There was no flow, andHasher.MemberID's only dispatch link was a test double incorrupt_test.go. Now it readsInitialCheck → Hasher.MemberID → EtcdServer.MemberID (server/etcdserver/server.go:1728), throughhasherAdapter.Cost: I timed the pass alone, from each arm's build, on the same prometheus and etcd graphs, 21 alternating runs. Medians: prometheus 258 → 272 ms, etcd 206 → 221 ms. It makes about 7% more edge reads (prometheus 2074 → 2212, etcd 1548 → 1665), all of them for Go structs with a synthesized
implementsedge.Known limits
go-implements. When it links a struct to an interface the struct doesn't really satisfy, the promoted method is linked as well; 6 of the 28 added edges are like that (above).promotedIntonames the first struct in node order when several structs promote the same method into one interface method.ChunkSeriesSet.Next's edge namesblockSeriesSet. The edge's target is the same either way.Overlap
#2419 (Go defined types as implementers) edits the same two functions, and whichever PR lands second resolves the conflict:
goImplementsEdges. fix(go): a defined type implements the interfaces its methods satisfy #2419 removes the localkindOfand keeps every Go type intypeKindup front, so this PR'sisEmbedding = (e) => isGoEmbedding(e, kindOf)conflicts. The equivalent line there isisGoEmbedding(e, (id) => typeKind.get(id) ?? null), with the same result.interfaceOverrideEdges. fix(go): a defined type implements the interfaces its methods satisfy #2419 only replaces the two loop headers, so that its walk also covers Gotype_alias. Keep both sides. Promotion here stays gated to structs, which is right: a defined type has no embedding edges.Another session (stdlib method sets for unresolved embeddings, not yet pushed) also touches
goImplementsEdges, so the same one-line resolution applies there.Tests
New
__tests__/go-promoted-dispatch.test.ts, 6 tests on a fixture shaped like prometheus:codegraph_explorelabel;5 of the 6 fail on
main; the sixth pins the provider's own edge. The tests pass on both the kernel and the wasm (CODEGRAPH_KERNEL=0) extraction paths.Full suite, in parallel, on this shared Windows box while other sessions held the CPU at 100%: 111 failures in 68 files.
EBUSYlocks.--testTimeout=120000: 1571 pass, 5 fail and 7 are skipped. That run included the synthesizer, resolution, type-hierarchy, explore and framework suites.daemon-pid-reuse,mcp-status-freshnessandmcp-writer-lock.function-ref's Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case, hits its own 60 s in-file timeout even alone. It does the same onmain's source.tsc --noEmitis clean.🤖 Generated with Claude Code