fix(go): implementing an interface needs matching parameter and result counts
Summary
goImplementsEdges (Go implicit interface satisfaction, #584) matched method names only. Go wants every method's signature identical, so a type whose same-named method takes or returns a different number of values can never satisfy the interface. Name matching linked it anyway:
- etcd's watch
Cache(Get(ctx, key string, opts ...OpOption) (*GetResponse, error)) "implemented" rafthttp'speerGetter(Get(id types.ID) Peer), along with 34 otherGets; - prometheus's test
Closer { Close() }collected 34 types whoseClosereturns an error; - on grpc-go, 38 balancer, resolver and logger builders whose
Buildtakes other arguments filled the 40-slot cap of xDS'sTransportBuilder(Build(ServerIdentifier) (Transport, error)) before the realgrpctransport.Builderwas reached, socodegraph_explorecould not follow the xDS client into the transport it builds.
Change
- Each wanted method now also needs a declaration, own or promoted, with the same parameter and result counts, read off the stored
signaturetext ((params) results). Parameters are the top-level comma-separated items (a, b intis two); results are none, one bare type, or a parenthesized list. Nested brackets (Pair[K, V],func(a, b int)), string literals (struct tags) and comments are skipped. A signature that doesn't read rules nothing out; on the five repos below every Go method signature read. - gRPC exception. A gRPC client interface (every method takes
opts ...grpc.CallOptionlast) also accepts its service's server arity: one parameter fewer with the same results for a unary RPC, one parameter fewer returningerrorfor a streaming one (the client's first result is not a pointer). SoKVClient.Range → kvServer.Range, which is no Go implementation but is the bridge a client's call crosses to reach its handler, stays. The generated client stub and real client wrappers keep matching by the client arity as before. - Types are still not compared, and a promoted name still counts even where the struct's own method of that name hides it (documented over-approximation).
Why keep the gRPC client → server pairs
They act as RPC bridges: codegraph_explore's Flow section follows a client call through the client interface into the server handler. With a plain arity gate those flows break, which is the partial-coverage case AGENTS.md warns about:
| explore probe (symbol bag) | main | plain gate | this PR |
|---|---|---|---|
etcd kv.Do KVClient.Range kvServer.Range EtcdServer.Range | Do → Do → KVClient.Range → kvServer.Range | stops at KVClient.Range | same as main |
etcd lessor.Grant LeaseClient.LeaseGrant LeaseServer.LeaseGrant EtcdServer.LeaseGrant | 5 hops to EtcdServer.LeaseGrant | detours through grpcproxy's ls2lc adapter | same as main |
etcd watchGRPCStream.openWatchClient WatchClient.Watch watchServer.Watch (bidi) | → watchServer.Watch | no flow | same as main |
grpc-go runRecordRoute RouteGuideClient.RecordRoute routeGuideServer.RecordRoute (client stream) | → routeGuideServer.RecordRoute | no flow | same as main |
cosmos-sdk request_Query_Balance_0 QueryClient.Balance BaseKeeper.Balance (REST gateway) | → BaseKeeper.Balance | no flow | same as main |
grpc-go LRSClient.getOrCreateLRSStream TransportBuilder.Build Builder.Build | no flow | → grpctransport.Builder.Build | → grpctransport.Builder.Build |
grpc-go XDSClient.getOrCreateChannel TransportBuilder.Build Builder.Build | no flow | same | same |
(The plain-gate column ran on main 31c3328d; main and this PR's columns were re-run on current main ed199e60 with identical Flow sections.) The dedicated goGrpcStubImplEdges pass doesn't cover this: it links Unimplemented*Server stub methods to implementations, not client interfaces to servers.
A first, looser exception (accept params - 1 with any result count) let any Set(a, b) error struct match cosmos-sdk's one-method KeyValueClient; deriving the server arity per RPC kind fixed that. Requiring the interface to live in a .pb.go file changed only 3 grpc-go edges (one is the server of checked-in generated code under another name), so it isn't required.
Validation
Kit: gin (43fe48e), prometheus (39c878f), etcd (76d58e3), grpc-go (6c0c296) and cosmos-sdk (2ad20ad), each indexed by current main (ed199e60) and by this branch, same kernel. The graphs differ only in go-implements edges and the interface-impl calls that hang off them; nodes, unresolved refs and files are identical.
| gin | prometheus | etcd | grpc-go | cosmos-sdk | |
|---|---|---|---|---|---|
go-implements | 93 → 92 | 1867 → 1641 | 928 → 638 | 2030 → 1671 | 6118 → 5135 |
| removed (struct / defined type) | 1 / 0 | 266 / 7 | 291 / 3 | 404 / 0 | 964 / 240 |
| added (cap slots freed) | 0 | 47 | 4 | 45 | 221 |
interface-impl | −1 | −397 +87 | −416 | −538 +57 | −2334 +203 |
- An independent re-derivation of the rule over the main graph agrees with every changed edge: all removed pairs fail it, all added pairs pass it. On main 31c3328d the real after-sets also matched a simulation of the whole pass (cap and ordering included) edge for edge.
- Removed — sampled per interface on all five repos; every one is a count mismatch Go would reject:
peerGetter.Get(35 on etcd),Compactable.Compact(the generatedkVClient.Compactwith call options),WatchClient.Watchvs clientv3'sWatch(ctx, key, opts...) WatchChan, printers'MemberAdd(resp), backend transactions'Lock()vsLockClient.Lock(ctx, in, opts...), prometheusCloser/AppenderV2/TBRun, grpc-go balancer builders underTransportBuilder/methodLoggerBuilder/ChannelCredentials, cosmos-sdk gomock recorders(arg0, arg1 any) *gomock.Calland the protobuffastReflection_*types underParamStore,HasValue,KeyValueServer. The 58 etcd gRPC-interface removals are none of them servers (the 5 keptWatchClientimplementers are exactly the generated client, the stub,watchServer,watchProxyand thews2wcadapter). - Added — the cap now fills with arity-plausible types: grpc-go's
grpctransport.Builderand fake transport underTransportBuilder, the xDS credential builders underChannelCredentials, prometheus discoverers underDiscoverer, and some same-count/other-type matches the cap used to hide (e.g.ProducerBuilder). - Signature judge (
exact/loose= signature text equal, up to package qualifiers):
| gin | prometheus | etcd | grpc-go | cosmos-sdk | |
|---|---|---|---|---|---|
| signatures equal | 92 → 92 | 1316 → 1337 | 526 → 527 | 1203 → 1229 | 3873 → 4043 |
| same counts, other types | 0 → 0 | 278 → 304 | 57 → 60 | 308 → 327 | 496 → 534 |
| counts differ: gRPC client → server (kept) | 0 → 0 | 0 → 0 | 102 → 44 | 121 → 115 | 627 → 549 |
| counts differ: other | 1 → 0 | 273 → 0 | 243 → 7 | 398 → 0 | 1122 → 9 |
The 16 left are a struct whose own method hides a promoted one of the same name but another shape (etcd's test integrationClient declares AuthEnable(ctx) error and embeds *clientv3.Client); the gate counts any declaration in the method set, as the name check already did.
- Cost: the pass issues exactly the same
QueryBuildercalls (counted per method on etcd, grpc-go and cosmos-sdk); the arity work only runs for pairs whose names already match. Alternating both builds in one process over the same stripped graph (15 rounds), the pass medians were etcd 253 → 266 ms, grpc-go 540 → 545, prometheus 308 → 290, cosmos-sdk 2865 → 2737 on main 31c3328d, and grpc-go 513 → 516, cosmos-sdk 4209 → 4414 on current main with a test run going: within this box's noise.
Agent A/B
Not run. The gRPC client→server flows come out byte-identical to main; the flows that changed are the two grpc-go xDS ones that now connect, plus side links in the "Dynamic-dispatch links" list (etcd's Compactable.Compact now shows a test fake instead of the client wrapper retryKVClient). A Windows port of ab-new-vs-baseline.sh (no rsync/jq here, global codegraph dropped from the arm PATH, node CLI-block hook) is ready, but every claude -p run on this machine fails with "OAuth session expired and could not be refreshed".
Tests
- New
__tests__/go-implements-arity.test.ts(22 cases): etcd/prometheus/grpc-go shapes (aGetwith other parameters, aClosewith a result, promoted methods, grouped names, multi-line signatures with comments, generics and struct tags, the cap-starvedTransportBuilder), a generated gRPC service with unary, server-streaming and bidi RPCs reaching the client stub, the unimplemented server and the real server (not a same-named wrapper, not via a mixed interface), andgoSignatureArityunit cases. All 22 fail on main and pass here, on the kernel and the wasm extraction paths. - All Go suites (16 files, 205 tests) pass on both paths; one wasm-path test (
go-bare-call-no-method) failed once in the parallel run and passed alone, on this branch and on main. - Full suite on this Windows box (busy): the parallel run passed 6497 and timed out 109 tests in 42 files; re-run serially with 120 s timeouts, those 42 files passed except one
function-ref.test.ts#1820 case at its own 60 s in-file limit, which passes alone here and on main.
Overlap
Merged current main (#2414 promoted dispatch, #2419 defined types, #2417 type aliases) into this branch: the gate applies to defined-type implementers too, and #2414's promoted links follow whatever go-implements edges remain.
Merged on top of
Main moved five times during the merge (#2430, #2442, #2444, #2433, #2449, #2434, #2448, #2437, #2431, #2441, #2436). Only #2430 touches this area: it drops Go struct-to-struct interface-impl edges, a set disjoint from the interface-based ones this PR changes. Re-indexing gin and etcd with the merged build differed from the earlier fix arm by exactly #2430's own removals (1 and 60 edges), so the tables above stand. #2441 adds references edges from defined types, which this pass never reads. On the merged tree (kernel rebuilt for #2441), the Go suites, including every incoming Go PR's tests, pass on both extraction paths (11 files, 168 tests).
Follow-ups (not here)
goGrpcStubImplEdgesmatches by name only as well: on etcd it linksUnimplementedKVServer.RangetoretryKVClient.Rangeandkvs2kvc.Range, client wrappers.- A struct's own method should hide a promoted one of the same name (16 edges above).
🤖 Generated with Claude Code