Skip to content

fix(go): a defined type implements the interfaces its methods satisfy - #2419

Merged
colbymchenry merged 7 commits into
mainfrom
claude/quirky-morse-bd9556
Oct 7, 2026
Merged

colbymchenry merged 7 commits into
mainfrom
claude/quirky-morse-bd9556

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

goImplementsEdges (Go implicit interface satisfaction, #584; method sets follow embeddings since #2402) offered only structs as implementers. A Go defined type over a slice, map, function or basic type declares methods the same way. It is extracted as a type_alias that owns its methods through contains edges, so it never satisfied an interface:

  • prometheus's type staticDiscoverer []*targetgroup.Group (Run) was not a Discoverer, and StaticConfig was not a discovery Config;
  • gin's formSource, headerSource and multipartRequest were not a setter;
  • etcd's Int64Comparable, StringComparable and the two affine comparables were not a Comparable;
  • an adapter like type HandlerFunc func(…) with ServeHTTP was not a Handler.

interfaceOverrideEdges walked only class, struct and union too, so even with the edge a call through the interface could not reach their methods.

  • Candidates: Go structs and Go type_alias nodes, read in one canonical (file_path, start_line, id) order. Only types with at least one method enter the per-interface loop. A method-less struct never covered an interface, so struct results are unchanged.
  • Cap ordering (fix(go): satisfying an interface counts the methods embedding brings in #2402): a defined type has no embeddings, so it declares every method it has. Under the per-interface cap it ranks with the structs that declare all of the interface's methods, in file order, ahead of structs that need promoted ones.
  • Dispatch bridge: interfaceOverrideEdges also walks Go type_alias nodes. The kind is walked for Go only, so nothing changes for other languages' aliases.
  • True aliases: type A = B is not extracted as a node, since tree-sitter-go parses it as type_alias, not type_spec. So it can never become a second implementer next to B. The fixture pins that, and none of the 21 = declarations in the three repos is a node.
  • A defined type over an interface (type Serving Handler) has no methods of its own and implements nothing.
  • Every Go type's embeddings now come from the batched prefetch, which removes the per-defined-type lazy lookups in kindOf and embeds.
  • iterateNodesByKindIn accepts several kinds, interleaved in its one order. A single kind keeps the kind = ? query.

Validation

The before arm is main at #2403 (31c3328, which includes #2402). The after arm is this branch. Both use the same kernel, built from that main, and the same extraction, on gin, prometheus and etcd (76d58e3). The same A/B on the earlier base, main at #2400 plus #2402, gave identical after-sets. So did re-runs after merging #2410 and #2411 (main d8a7f86, which changed Go import-name resolution) after merging #2414 (main 855eca2), and on main 3bf6b8f, through #2415 and #2416, which changed framework and Go type-position resolution. With #2414, its promoted-method interface-impl edges are the same with and without this change: 0, 17 and 11.

  • The graphs differ only in go-implements edges and the interface-impl calls that follow them. Nodes and unresolved refs are identical.
  • The after set matches, edge for edge, an independent simulation of the new rule run over the before graph.
gin prometheus etcd
go-implements 83 → 93 (+10) 1829 → 1866 (+45, −8) 921 → 928 (+7)
interface-impl (Go) 170 → 180 (+10) 3366 → 3437 (+80, −9) 2607 → 2614 (+7)

Added: every added go-implements edge has a defined type as its source. Examples:

  • gin: formSource, headerSource and multipartRequest → setter; test types → BindUnmarshaler.
  • prometheus: staticDiscoverer and lockStaticDiscoverer → Discoverer, StaticConfig → Config, QueryableFunc → Queryable, Expressions and TestStmt → parser Node, inspector → Visitor, realByteSlice → the three ByteSlices, warningsOnlySeriesSet → genericSeriesSet.
  • etcd: the four comparables → Comparable.

Every added interface-impl edge hangs off one of these, for example Discoverer.Run → staticDiscoverer.Run and setter.TrySet → formSource.TrySet.

Removed (prometheus only): each of the 8 is a cap displacement. They are at interfaces that already had more than 40 qualifying implementers, where a defined type earlier in file order now takes the slot:

  • Discoverer: robotDiscovery and http Discovery out, staticDiscoverer and lockStaticDiscoverer in. All four are real implementers. The two displaced ones only get Run promoted from *refresh.Discovery, so they had no dispatch edges, and the type hierarchy still lists them one level down under refresh.Discovery.
  • TBRun (testing.TB + Run(string, func(*testing.T)) bool): the same two out, the same two in. All four are name-only matches, because the unresolved testing.TB truncates the interface (a known limit since fix(go): satisfying an interface counts the methods embedding brings in #2402).
  • BucketIterator, Iterator, StringIter: chunkedSeriesIterator / listSeriesIterator (At() (int64, float64)) out, warningsOnlySeriesSet (At() Labels) in. All are name-only matches.
  • genericSeriesSet: blockChunkSeriesSet (At() ChunkSeries, a name-only match) out, warningsOnlySeriesSet (At() Labels, an exact match) in.
  • The 9 removed interface-impl edges hang off those.

Precision. Every added pair was checked against the methods' signature text, after dropping parameter names. The rate matches the struct edges in the same graphs, since both sides use the same name-only rule:

gin prometheus etcd
defined types: signatures equal 10 / 10 30 / 45 (7 of them up to a package qualifier) 4 / 7
structs (unchanged): signatures equal 82 / 83 1285 / 1821 522 / 921

The mismatches are the same kind as for structs:

  • warningsOnlySeriesSet (At() Labels) also matches SeriesSet, ChunkSeriesSet, BucketIterator, Iterator and StringIter by name.
  • etcd's StringsValue, URLsValue and uint32Value have Set(string) error, but flagSetter wants Set(fk, fv string) error. None of its 12 struct matches from before this change has that signature either: seven are flag.Value-style, one is ready.Set(), and four are the v2 store's four-argument Set, two of them promoted.
  • prometheus's Selector (Matches(Labels)) matches StringMatcher (Matches(string)), and Annotations and Intervals match unsafeLabelAdder on Add.

Effect on answers:

  • codegraph_explore on gin, query mappingByPtr tryToSetValue setter.TrySet formSource.TrySet setByForm. Before, the Flow ended at setter.TrySet. After, it continues through the interface dispatch into an implementation and on to setByForm, and lists the other implementation link.
  • Type hierarchy:
    • etcd Comparable: 2 → 6 implementers;
    • gin setter: 0 → 3;
    • prometheus discovery Config: 38 → 39;
    • prometheus Discoverer keeps its 40, now including staticDiscoverer.
  • Dead code: the reported lists are identical on all three repos. Fewer symbols reach the exclusion rules as candidates (gin 797 → 789, prometheus 3820 → 3784, etcd 3696 → 3693), because the defined types' interface methods now have callers.

Cost: I timed the pass alone, on graphs with synthesized edges stripped, interleaving the arms, 21 runs each.

  • Medians: prometheus 308 → 320 ms, etcd 317 → 357 ms.
  • Six more interleaved etcd rounds overlapped (round medians 301–472 ms before, 321–433 ms after). This machine is shared and noisy.
  • The extra work is the method reads for the defined types (179 on prometheus, 182 on etcd). The per-interface loop now skips types without methods.

Known limits, unchanged here

  • Name-only matching, as for structs (see Precision). 342 of etcd's 921 struct edges differ in arity, which Go never accepts. They are mostly single-method interfaces with common names, like peerGetter.Get and Compactable.Compact. 102 are gRPC server structs matched to the generated *Client interfaces, and those currently bridge RPC flows. An arity check is a separate decision with that trade-off; I filed a follow-up to measure it.
  • Methods declared through an alias receiver aren't attached. In prometheus, type KumaSDConfig = SDConfig has 5 methods declared through it, and they stay unattached, so SDConfig misses discovery Config. This is part of the known gap that = aliases are not extracted.
  • Promoted methods of type T S, where S is a struct, are not modeled. In Go, T keeps the methods promoted through S's embedded fields. T has no embedding edges here, so it is matched by its own methods only.
  • Explore's Flow takes the first-enumerated implementation at an interface dispatch, even when the query names another. On gin it went through headerSource.TrySet although formSource.TrySet was named. This comes from the flow walk itself and applies to struct implementations equally. I filed a follow-up.

Tests

Overlap

#2414 landed first. It also edits interfaceOverrideEdges and turned goImplementsEdges' isEmbedding into the module-level isGoEmbedding(e, kindOf). Here is how the merge resolves:

🤖 Generated with Claude Code

goImplementsEdges (#584) offered only structs as implementers. A Go defined
type over a slice, map, function or basic type declares methods the same
way (gin's `formSource map[string][]string` has TrySet, prometheus's
`staticDiscoverer []*targetgroup.Group` has Run, an adapter
`HandlerFunc func(...)` has ServeHTTP) and is extracted as a `type_alias`
that owns them through go-method-contains edges. So it never satisfied an
interface, and interfaceOverrideEdges, which walks only class, struct and
union, could not link a call through the interface to its methods.

Structs and defined types are now candidates together, in one canonical
(file, line) order. A defined type declares every method it has, so under
the per-interface cap it ranks with the structs that declare theirs, ahead
of promoted-only ones. interfaceOverrideEdges also walks Go `type_alias`
nodes. Every Go type's embeddings come from the batched prefetch (the
per-type lazy lookup for defined types is gone), and a type without a
method is dropped before the per-interface loop. A true alias
(`type A = B`) is not extracted, so it never implements anything itself.

iterateNodesByKindIn takes several kinds, interleaved in its one order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 2f2afea into main Oct 7, 2026
colbymchenry added a commit that referenced this pull request Oct 7, 2026
…satisfy

With #2419, goImplementsEdges offers every Go type_alias as an
implementer. A true alias now has a node, and owns the methods written
with it as the receiver (prometheus's func (c *KumaSDConfig) Name(),
KumaSDConfig = SDConfig), which Go gives the aliased type; so the alias
implements discovery.Config in SDConfig's place. The doc comment said a
true alias is not extracted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 7, 2026
…t names (#2417)

tree-sitter-go parses `type Event = mvccpb.Event` as a `type_alias`, beside
the `type_spec` of a defined type, and both extractors read `type_spec`
only. etcd's client/v3 `Event` had no node; a use through its package
(`clientv3.Event`, prometheus's `storage.AOptions{…}`) linked to nothing,
and a use inside it linked by name to any type so named, another package's
included.

- Extraction (TS and kernel): an alias is a `type_alias` node referencing
  each type its right-hand side names, at the name so resolution reads the
  package back; an alias of a struct or interface literal is a struct or
  interface. The alias's own type parameters and Go's predeclared types are
  skipped. A generic alias, which tree-sitter-go 0.23 parses as a
  `type_spec` around an error, reads as an alias on wasm; the kernel defers
  its file.
- A method called on an alias-typed value is the aliased type's: an alias
  of a project type is followed in its own package, through pointers and
  further aliases; one of an outside type resolves by name as before.
- An alias's target written through a package that is none of the file's
  imports as indexed stays unresolved, as an embedded type's does, so the
  alias never links to itself or a namesake.
- With Go defined types as implementers (#2419), an alias that owns
  methods written with it as the receiver implements what they satisfy,
  in place of the type it names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 7, 2026
…t counts (#2447)

goImplementsEdges matched method names only, so a struct whose same-named
method takes or returns a different number of values, which Go can never
accept, was linked as an implementation (etcd's watch cache as a
peerGetter), and enough such look-alikes filled the per-interface cap
before the real implementer (grpc-go's xDS TransportBuilder).

Each wanted method now also needs a declaration with its parameter and
result counts, read off the stored signature text; a signature that
doesn't read rules nothing out. A gRPC client interface (every method
takes `opts ...grpc.CallOption` last) also accepts its RPCs' server
arities, so `KVClient.Range -> kvServer.Range`, the bridge explore
follows from a client call to its handler, stays. Defined types (#2419)
are held to the same counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant