Repository navigation
fix(go): a defined type implements the interfaces its methods satisfy - #2419
Merged
Merged
Conversation
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>
…d9556 # Conflicts: # CHANGELOG.md # src/resolution/callback-synthesizer.ts
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>
This was referenced Oct 7, 2026
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>
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
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 atype_aliasthat owns its methods throughcontainsedges, so it never satisfied an interface:type staticDiscoverer []*targetgroup.Group(Run) was not aDiscoverer, andStaticConfigwas not a discoveryConfig;formSource,headerSourceandmultipartRequestwere not asetter;Int64Comparable,StringComparableand the two affine comparables were not aComparable;type HandlerFunc func(…)withServeHTTPwas not aHandler.interfaceOverrideEdgeswalked only class, struct and union too, so even with the edge a call through the interface could not reach their methods.type_aliasnodes, 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.interfaceOverrideEdgesalso walks Gotype_aliasnodes. The kind is walked for Go only, so nothing changes for other languages' aliases.type A = Bis not extracted as a node, since tree-sitter-go parses it astype_alias, nottype_spec. So it can never become a second implementer next toB. The fixture pins that, and none of the 21=declarations in the three repos is a node.type Serving Handler) has no methods of its own and implements nothing.kindOfandembeds.iterateNodesByKindInaccepts several kinds, interleaved in its one order. A single kind keeps thekind = ?query.Validation
The before arm is
mainat #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-methodinterface-impledges are the same with and without this change: 0, 17 and 11.go-implementsedges and theinterface-implcalls that follow them. Nodes and unresolved refs are identical.go-implementsinterface-impl(Go)Added: every added
go-implementsedge has a defined type as its source. Examples:formSource,headerSourceandmultipartRequest→setter; test types →BindUnmarshaler.staticDiscovererandlockStaticDiscoverer→Discoverer,StaticConfig→Config,QueryableFunc→Queryable,ExpressionsandTestStmt→ parserNode,inspector→Visitor,realByteSlice→ the threeByteSlices,warningsOnlySeriesSet→genericSeriesSet.Comparable.Every added
interface-impledge hangs off one of these, for exampleDiscoverer.Run → staticDiscoverer.Runandsetter.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:robotDiscoveryand httpDiscoveryout,staticDiscovererandlockStaticDiscovererin. All four are real implementers. The two displaced ones only getRunpromoted from*refresh.Discovery, so they had no dispatch edges, and the type hierarchy still lists them one level down underrefresh.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 unresolvedtesting.TBtruncates 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.interface-impledges 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:
The mismatches are the same kind as for structs:
warningsOnlySeriesSet(At() Labels) also matchesSeriesSet,ChunkSeriesSet,BucketIterator,IteratorandStringIterby name.StringsValue,URLsValueanduint32ValuehaveSet(string) error, butflagSetterwantsSet(fk, fv string) error. None of its 12 struct matches from before this change has that signature either: seven areflag.Value-style, one isready.Set(), and four are the v2 store's four-argumentSet, two of them promoted.Selector(Matches(Labels)) matchesStringMatcher(Matches(string)), andAnnotationsandIntervalsmatchunsafeLabelAdderonAdd.Effect on answers:
codegraph_exploreon gin, querymappingByPtr tryToSetValue setter.TrySet formSource.TrySet setByForm. Before, the Flow ended atsetter.TrySet. After, it continues through the interface dispatch into an implementation and on tosetByForm, and lists the other implementation link.Comparable: 2 → 6 implementers;setter: 0 → 3;Config: 38 → 39;Discovererkeeps its 40, now includingstaticDiscoverer.Cost: I timed the pass alone, on graphs with synthesized edges stripped, interleaving the arms, 21 runs each.
Known limits, unchanged here
peerGetter.GetandCompactable.Compact. 102 are gRPC server structs matched to the generated*Clientinterfaces, 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.type KumaSDConfig = SDConfighas 5 methods declared through it, and they stay unattached, soSDConfigmisses discoveryConfig. This is part of the known gap that=aliases are not extracted.type T S, whereSis 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.headerSource.TrySetalthoughformSource.TrySetwas named. This comes from the flow walk itself and applies to struct implementations equally. I filed a follow-up.Tests
__tests__/go-implements-defined-types.test.ts, 6 tests, over a fixture shaped like prometheus discovery, gin binding (with a method in another file), etcdadt, an adapter func type and a generic defined type. It covers:go-implementsset;interface-implto the defined types' methods;main. They pass on both the kernel and the wasm (CODEGRAPH_KERNEL=0) extraction paths, as does fix(go): satisfying an interface counts the methods embedding brings in #2402'sgo-implements-embedding.test.ts.tsc --noEmitis clean.EBUSY, and 8 were daemon-lock waits or timing thresholds.--no-file-parallelism, 120 s timeouts): 2081 passed and 4 failed. Each of the 4 was then run alone:mcp-writer-lockandmpeg-ts-not-typescriptpass.daemon-pid-reusepassed twice.function-ref's Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case passed once and once hit its own 60 s timeout, at 60.7 s. It also passed on main's code. Its fixtures are Python only, so neithergo-implementsnor the interface-dispatch pass runs in it.go-promoted-dispatch,go-implements-embedding,go-interface-embedding,go-type-position-kinds,go-import-package-names,go-framework-own-packageandtype-hierarchy. On the final merge through fix(sync): a navigation call links its route when the route appears later #2425, the first three andtype-hierarchypass again.sync,sync-import-retryandresolutionpassed on the earlier merge through test(sync): compare synced graphs with an index rebuilt from scratch #2411.dead-code.test.tspasses 31/31 on the wasm path. fix(dead-code): a type that declare module or declare global merges outward is not dead #2418's newdeclare modulecases need a kernel rebuilt from main, and mine was built before that change.Overlap
#2414 landed first. It also edits
interfaceOverrideEdgesand turnedgoImplementsEdges'isEmbeddinginto the module-levelisGoEmbedding(e, kindOf). Here is how the merge resolves:isEmbeddingstays exactlyisGoEmbedding(e, kindOf). OnlykindOfchanges: it is now a plain read of the prefetched type map, because every Go type is intypeKindup front.interfaceOverrideEdgeskeeps both changes. fix(go): a call through an interface reaches a method the struct gets by embedding #2414'sgoStructgate keeps promotion to structs, which is right, since a defined type has no embeddings.🤖 Generated with Claude Code