From d510484ada0e910ce6f1ba60c828ee2b5b258e94 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 6 Oct 2026 22:59:55 -0500 Subject: [PATCH] fix(go): a defined type implements the interfaces its methods satisfy 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 --- CHANGELOG.md | 1 + __tests__/go-implements-defined-types.test.ts | 315 ++++++++++++++++++ src/db/queries.ts | 14 +- src/resolution/callback-synthesizer.ts | 103 +++--- 4 files changed, 380 insertions(+), 53 deletions(-) create mode 100644 __tests__/go-implements-defined-types.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1144fe4..f6a2d6f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. - In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. +- In Go, a type defined over a slice, map, function or basic type now counts as implementing the interfaces its methods satisfy, just as a struct does: prometheus's `type staticDiscoverer []*targetgroup.Group` is a `Discoverer`, gin's `type formSource map[string][]string` a `setter`, etcd's `type Int64Comparable int64` a `Comparable`, and an adapter like `type HandlerFunc func(…)` a `Handler`. Before, only structs were considered, so these types were missing from an interface's implementations in `codegraph_explore`, impact and the type hierarchy, and a call through the interface, like `Discoverer.Run` or `setter.TrySet`, stopped there instead of reaching their methods. Re-index Go projects after upgrading. - In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading. - React Router routes kept in a table of their own are now indexed: the ASP.NET Core React template's `AppRoutes` array that `App.js` maps into ``, a `routes` array imported into `useRoutes(routes)` or `createBrowserRouter(routes)` or returned by a function as in `useRoutes(routes(isLoggedIn))`, route objects kept one per file and listed in `createBrowserRouter([MainRoutes, LoginRoutes])`, and routes written in place in `useRoutes([…])`. Before, route objects were read only from a file that itself creates a data router, so these apps had no routes, and their `navigate('/login')` calls and `` links led nowhere. Each route links to the page its `element` renders, past a guard like `` or ``, and through `lazy(() => import(…))` to the page it loads rather than a same-named page elsewhere in the repository. An `index: true` route is the page at its parent's address, and a parent route around others counts as their layout. A `{ path, element }` list that nothing hands to the router, such as a menu, still makes no routes. Re-index React projects after upgrading. diff --git a/__tests__/go-implements-defined-types.test.ts b/__tests__/go-implements-defined-types.test.ts new file mode 100644 index 000000000..66a2f30b2 --- /dev/null +++ b/__tests__/go-implements-defined-types.test.ts @@ -0,0 +1,315 @@ +/** + * Go's implicit interface satisfaction (#584) is not only for structs. Any + * defined type can declare methods, and plenty of real implementers are not + * structs at all: + * + * type staticDiscoverer []*targetgroup.Group // prometheus discovery + * func (c staticDiscoverer) Run(ctx context.Context, up chan<- []*targetgroup.Group) + * + * type formSource map[string][]string // gin binding + * func (form formSource) TrySet(…) (bool, error) + * + * type Int64Comparable int64 // etcd pkg/adt + * func (v Int64Comparable) Compare(c Comparable) int + * + * Such a type is extracted as a `type_alias` that owns its methods through + * `contains` edges, but goImplementsEdges only ever offered structs as + * implementers, and the interface-dispatch bridge only walked class, struct and + * union. So `Discoverer` never listed the static discoverer, and a call + * through `Discoverer.Run` could not reach its `Run`. + */ +import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import type { Edge, Node } from '../src/types'; + +const RING_STRUCTS = 40; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // prometheus's discovery package. + 'discovery/discovery.go': `package discovery + +import "context" + +type Group struct{ Source string } + +type Discoverer interface { + Run(ctx context.Context, up chan<- []*Group) +} + +type Config interface { + Name() string + NewDiscoverer() (Discoverer, error) +} + +// A list of groups that is a Config, and hands out a Discoverer over itself. +type StaticConfig []*Group + +func (StaticConfig) Name() string { return "static" } + +func (c StaticConfig) NewDiscoverer() (Discoverer, error) { return staticDiscoverer(c), nil } + +type staticDiscoverer []*Group + +func (c staticDiscoverer) Run(ctx context.Context, up chan<- []*Group) { + select { + case <-ctx.Done(): + case up <- c: + } +} + +// A true alias: the same type as staticDiscoverer, not a second one. +type StaticDiscoverer = staticDiscoverer + +// A name but no NewDiscoverer: no Config. +type namedGroups []*Group + +func (namedGroups) Name() string { return "named" } +`, + // gin's binding package: two setters over a map and over a struct type, the + // second with its method in another file. + 'binding/form_mapping.go': `package binding + +type setOptions struct{ isDefaultExists bool } + +type setter interface { + TrySet(key string, opt setOptions) (bool, error) +} + +type formSource map[string][]string + +func (form formSource) TrySet(key string, opt setOptions) (bool, error) { return false, nil } + +// No method at all. +type keys []string +`, + 'binding/multipart_form_mapping.go': `package binding + +import "net/http" + +type multipartRequest http.Request +`, + 'binding/multipart_set.go': `package binding + +func (r *multipartRequest) TrySet(key string, opt setOptions) (bool, error) { return false, nil } +`, + // etcd's pkg/adt. + 'adt/interval_tree.go': `package adt + +type Comparable interface { + Compare(c Comparable) int +} + +type StringComparable string + +func (s StringComparable) Compare(c Comparable) int { return 0 } + +type Int64Comparable int64 + +func (v Int64Comparable) Compare(c Comparable) int { return 0 } +`, + // An adapter over a function type, a generic defined type, and a defined + // type whose underlying type is an interface. + 'web/handler.go': `package web + +type Request struct{} + +type ResponseWriter interface { + Write(b []byte) (int, error) +} + +type Handler interface { + ServeHTTP(w ResponseWriter, r *Request) +} + +type HandlerFunc func(ResponseWriter, *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } + +type Lener interface { + Len() int +} + +type Set[T comparable] map[T]struct{} + +func (s Set[T]) Len() int { return len(s) } + +// An interface type by another name: never an implementer itself. +type Serving Handler +`, + // More implementers that declare Tick than the cap: a defined type in the + // file listed first, then the structs. + 'ring/a.go': 'package ring\n\ntype Ticker interface{ Tick() }\n\ntype alarm int\n\nfunc (alarm) Tick() {}\n', + 'ring/b.go': [ + 'package ring', + '', + ...Array.from({ length: RING_STRUCTS }, (_, i) => { + const name = `tick${String(i).padStart(2, '0')}`; + return `type ${name} struct{}\n\nfunc (${name}) Tick() {}\n`; + }), + ].join('\n'), +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-defined-types-')); + for (const [rel, content] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); +}, 120_000); + +afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +const meta = (e: Edge): Record => (e.metadata ?? {}) as Record; +const pkg = (n: Node | null): string => `${path.posix.dirname(n?.filePath ?? '?')}.${n?.name}`; + +/** The one non-import node with this name in this file. */ +function one(name: string, file: string): Node { + const found = cg.getNodesByName(name).filter((n) => n.filePath === file && n.kind !== 'import'); + expect(found, `${name} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** Every Go type in the graph that could implement an interface. */ +function goTypes(g: CodeGraph): Node[] { + return [...g.getNodesByKind('struct'), ...g.getNodesByKind('type_alias')].filter((n) => n.language === 'go'); +} + +/** Every synthesized type → interface edge, as `pkg.Type -> pkg.Interface`. */ +function goImplements(): string[] { + return cg + .getOutgoingEdgesFrom(goTypes(cg).map((n) => n.id), ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => `${pkg(cg.getNode(e.source))} -> ${pkg(cg.getNode(e.target))}`) + .filter((e) => !e.startsWith('ring.')) + .sort(); +} + +/** Where a call through the interface's method is bridged to. */ +function dispatchTargets(iface: string, file: string, method: string): string[] { + const m = cg + .getOutgoingEdgesFrom([one(iface, file).id], ['contains']) + .map((e) => cg.getNode(e.target)) + .find((n) => n?.name === method)!; + return cg + .getOutgoingEdgesFrom([m.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => cg.getNode(e.target)?.qualifiedName ?? '?') + .sort(); +} + +describe('Go implicit satisfaction counts defined types as implementers', () => { + it('links every defined type whose methods cover the interface', () => { + expect(goImplements()).toEqual([ + 'adt.Int64Comparable -> adt.Comparable', + 'adt.StringComparable -> adt.Comparable', + 'binding.formSource -> binding.setter', + 'binding.multipartRequest -> binding.setter', + 'discovery.StaticConfig -> discovery.Config', + 'discovery.staticDiscoverer -> discovery.Discoverer', + 'web.HandlerFunc -> web.Handler', + 'web.Set -> web.Lener', + ]); + }); + + it('bridges a call through the interface to the defined type\'s method', () => { + expect(dispatchTargets('Discoverer', 'discovery/discovery.go', 'Run')).toEqual(['staticDiscoverer::Run']); + expect(dispatchTargets('Config', 'discovery/discovery.go', 'NewDiscoverer')).toEqual([ + 'StaticConfig::NewDiscoverer', + ]); + expect(dispatchTargets('setter', 'binding/form_mapping.go', 'TrySet')).toEqual([ + 'formSource::TrySet', + 'multipartRequest::TrySet', + ]); + expect(dispatchTargets('Comparable', 'adt/interval_tree.go', 'Compare')).toEqual([ + 'Int64Comparable::Compare', + 'StringComparable::Compare', + ]); + expect(dispatchTargets('Handler', 'web/handler.go', 'ServeHTTP')).toEqual(['HandlerFunc::ServeHTTP']); + }); + + it('shows them in the type hierarchy, both ways', () => { + const below = buildTypeHierarchy(cg, one('Comparable', 'adt/interval_tree.go'))?.descendants ?? []; + expect(below.map((d) => `${d.relation} ${pkg(d.node)}${d.synthesized ? ' (synthesized)' : ''}`).sort()).toEqual([ + 'implements adt.Int64Comparable (synthesized)', + 'implements adt.StringComparable (synthesized)', + ]); + const above = buildTypeHierarchy(cg, one('staticDiscoverer', 'discovery/discovery.go'))?.ancestors ?? []; + expect(above.map((a) => `${a.relation} ${pkg(a.node)}`)).toEqual(['implements discovery.Discoverer']); + }); + + it('keeps a true alias and an interface by another name from implementing anything', () => { + const named = (name: string) => + cg + .getNodesByName(name) + .filter((n) => n.language === 'go' && n.kind !== 'import') + .flatMap((n) => cg.getOutgoingEdgesFrom([n.id], ['implements'])); + expect(named('StaticDiscoverer')).toEqual([]); + expect(named('Serving')).toEqual([]); + // The type the alias names is linked once. + expect(goImplements().filter((e) => e.includes('staticDiscoverer'))).toHaveLength(1); + }); + + it('caps an interface at 40 implementers, taking defined types in file order with the declaring structs', () => { + const linked = cg + .getIncomingEdgesTo([one('Ticker', 'ring/a.go').id], ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => cg.getNode(e.source)!.name); + // alarm and 40 structs declare Tick. alarm comes first in file order, so + // the last struct is the one the cap leaves out. + expect(linked).toHaveLength(40); + expect(linked).toContain('alarm'); + expect(linked).not.toContain(`tick${RING_STRUCTS - 1}`); + expect(dispatchTargets('Ticker', 'ring/a.go', 'Tick')).toContain('alarm::Tick'); + }); +}); + +describe('a sync that edits a defined type\'s method', () => { + let dir = ''; + let graph: CodeGraph | undefined; + + afterEach(() => { + graph?.destroy(); + graph = undefined; + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = ''; + }); + + it('re-derives what the defined type satisfies', async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-defined-types-sync-')); + const write = (rel: string, content: string) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), content); + }; + const int64 = 'package adt\n\ntype Int64Comparable int64\n\nfunc (v Int64Comparable) Compare(c Comparable) int { return 0 }\n'; + write('go.mod', 'module example.com/app\n\ngo 1.22\n'); + write('adt/comparable.go', 'package adt\n\ntype Comparable interface {\n\tCompare(c Comparable) int\n}\n'); + write('adt/int64.go', int64); + graph = await CodeGraph.init(dir, { index: true }); + const g = graph; + const linked = () => + g + .getOutgoingEdgesFrom(goTypes(g).map((n) => n.id), ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => `${g.getNode(e.source)?.name} -> ${g.getNode(e.target)?.name}`); + + expect(linked()).toEqual(['Int64Comparable -> Comparable']); + write('adt/int64.go', int64.replace('Compare(', 'Cmp(')); + await g.sync({ paths: ['adt/int64.go'] }); + expect(linked()).toEqual([]); + write('adt/int64.go', int64); + await g.sync({ paths: ['adt/int64.go'] }); + expect(linked()).toEqual(['Int64Comparable -> Comparable']); + }, 60_000); +}); diff --git a/src/db/queries.ts b/src/db/queries.ts index 77fef9bba..f96494243 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -1333,14 +1333,18 @@ export class QueryBuilder { * order — the ORDER BY is total (`id` is unique), so this yields exactly the * nodes a caller filtering iterateNodesByKind by language would keep, in the * same sequence. A Go pass on a TypeScript monorepo otherwise materialized - * every method in the project to find a couple of Go ones. + * every method in the project to find a couple of Go ones. Several kinds + * come interleaved in that one order (the sort then holds only the rows + * the language filter keeps). */ - *iterateNodesByKindIn(kind: NodeKind, languages: readonly string[]): IterableIterator { - if (languages.length === 0) return; + *iterateNodesByKindIn(kind: NodeKind | readonly NodeKind[], languages: readonly string[]): IterableIterator { + const kinds: readonly NodeKind[] = typeof kind === 'string' ? [kind] : kind; + if (kinds.length === 0 || languages.length === 0) return; + const kindTest = kinds.length === 1 ? 'kind = ?' : `kind IN (${kinds.map(() => '?').join(', ')})`; const stmt = this.db.prepare( - `SELECT * FROM nodes WHERE kind = ? AND language IN (${languages.map(() => '?').join(', ')}) ORDER BY file_path, start_line, id` + `SELECT * FROM nodes WHERE ${kindTest} AND language IN (${languages.map(() => '?').join(', ')}) ORDER BY file_path, start_line, id` ); - for (const row of stmt.iterate(kind, ...languages)) { + for (const row of stmt.iterate(...kinds, ...languages)) { yield rowToNode(row as NodeRow); } } diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index ebecb41a8..125f902e0 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -804,12 +804,21 @@ const IFACE_OVERRIDE_LANGS = new Set([ ]); /** * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a - * struct satisfies an interface structurally when its method set covers the - * interface's. Synthesize the missing `implements` edge (struct → interface) by + * type satisfies an interface structurally when its method set covers the + * interface's. Synthesize the missing `implements` edge (type → interface) by * matching method-NAME sets, so impl-navigation works and the interface-dispatch * bridge ({@link interfaceOverrideEdges}, now 'go'-enabled) can link an interface * method call to the concrete overrides. * + * The implementers are structs and defined types. A defined type declares + * methods as a struct does — gin's `type formSource map[string][]string` has + * `TrySet`, prometheus's `type staticDiscoverer []*targetgroup.Group` has `Run`, + * an adapter `type HandlerFunc func(…)` has `ServeHTTP` — and is extracted as a + * `type_alias` that owns them through `contains` edges. Go gives it none of + * the methods declared on the type it is written over, so it is matched by + * the methods it declares. A true alias (`type A = B`) is B, not a type of its + * own, and is not extracted as one. + * * Both method sets include what embedding brings in, read off the declared * `extends`/`implements` edge each embedded type is. An interface has the * methods of the interfaces it embeds: etcd's `AuthReadTx` is `RLock` and @@ -824,7 +833,7 @@ const IFACE_OVERRIDE_LANGS = new Set([ * where Go finds it ambiguous or hidden by a field) — over-approximation * accepted, in line with the other dispatch synthesizers; capped per * interface. Empty interfaces (`any`) are skipped so they don't match every - * struct, and a struct that embeds the interface keeps the edge its embedding + * type, and a struct that embeds the interface keeps the edge its embedding * already is. */ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise { @@ -832,13 +841,15 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr const edges: Edge[] = []; const seen = new Set(); - // Materializes GO structs and interfaces only (the pass is language-gated by - // the caller), never the whole struct kind — that array is O(nodes) on - // struct-heavy repos like the Linux kernel (#1212). - const goStructs: Node[] = []; - for (const s of queries.iterateNodesByKindIn('struct', ['go'])) { + // Materializes GO types only (the pass is language-gated by the caller), + // never the whole struct kind — that array is O(nodes) on struct-heavy repos + // like the Linux kernel (#1212). Structs and defined types arrive + // interleaved in one canonical order, so the cap below takes implementers + // as the files declare them, whatever their kind. + const goImplementers: Node[] = []; + for (const n of queries.iterateNodesByKindIn(['struct', 'type_alias'], ['go'])) { if ((++scanned255 & 63) === 0) await onYield(); - goStructs.push(s); + goImplementers.push(n); } const goInterfaces: Node[] = []; for (const i of queries.iterateNodesByKindIn('interface', ['go'])) { @@ -846,20 +857,11 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr goInterfaces.push(i); } - // The kinds of type whose methods embedding passes on. A defined type - // (`type HandlersChain []HandlerFunc`) is looked up when an embedding names one. - const typeKind = new Map(); - for (const s of goStructs) typeKind.set(s.id, 'struct'); + // The kinds of type whose methods embedding passes on: a struct can embed a + // defined type (`type HandlersChain []HandlerFunc`) as well. + const typeKind = new Map(); + for (const n of goImplementers) typeKind.set(n.id, n.kind); for (const i of goInterfaces) typeKind.set(i.id, 'interface'); - const kindOf = (id: string): NodeKind | null => { - let kind = typeKind.get(id); - if (kind === undefined) { - const n = queries.getNodeById(id); - kind = n?.language === 'go' && n.kind === 'type_alias' ? 'type_alias' : null; - typeKind.set(id, kind); - } - return kind; - }; // Memoized: an embedded base is read once, however many types embed it. const ownMemo = new Map>(); @@ -879,11 +881,13 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr }; // The types one embeds: an interface embeds interfaces, a struct any of the - // three. Every struct's and interface's are read up front in a few batched - // queries rather than one query per type; a defined type's, when reached. - const isEmbedding = (e: Edge): boolean => - e.provenance !== 'heuristic' && - (typeKind.get(e.source) === 'interface' ? kindOf(e.target) === 'interface' : kindOf(e.target) !== null); + // three. Every type's are read up front in a few batched queries rather than + // one query per type. + const isEmbedding = (e: Edge): boolean => { + if (e.provenance === 'heuristic') return false; + const target = typeKind.get(e.target); + return typeKind.get(e.source) === 'interface' ? target === 'interface' : target !== undefined; + }; const NO_EMBEDS: string[] = []; const embedMemo = new Map(); const typeIds = [...typeKind.keys()]; @@ -896,15 +900,7 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr } await onYield(); } - const embeds = (id: string): string[] => { - let targets = embedMemo.get(id); - if (targets) return targets; - const kind = typeKind.get(id); - if (kind === 'struct' || kind === 'interface') return NO_EMBEDS; - targets = queries.getOutgoingEdges(id, ['extends', 'implements']).filter(isEmbedding).map((e) => e.target); - embedMemo.set(id, targets); - return targets; - }; + const embeds = (id: string): string[] => embedMemo.get(id) ?? NO_EMBEDS; // Own methods plus every embedded type's, down to the last level. Go allows // a struct to embed a pointer to itself, or to one embedding it back. @@ -933,10 +929,15 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr return true; }; - const structMethods = new Map>(); - for (const s of goStructs) { + // A type without a method satisfies no interface this pass looks at. + const candidates: Node[] = []; + const candidateMethods = new Map>(); + for (const s of goImplementers) { if ((++scanned255 & 63) === 0) await onYield(); - structMethods.set(s.id, methodSet(s.id)); + const have = methodSet(s.id); + if (have.size === 0) continue; + candidates.push(s); + candidateMethods.set(s.id, have); } for (const iface of goInterfaces) { @@ -958,14 +959,14 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr }); added++; }; - // A struct that needs promoted methods waits for those declaring all of - // them: under the cap, those are what the interface-dispatch bridge links - // a call through the interface to. + // A struct that needs promoted methods waits for the types declaring all + // of them, every defined type among them: under the cap, those are what + // the interface-dispatch bridge links a call through the interface to. const throughEmbedding: Node[] = []; - for (const s of goStructs) { + for (const s of candidates) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; - const have = structMethods.get(s.id); - if (!have || !covers(have, want)) continue; + const have = candidateMethods.get(s.id)!; + if (!covers(have, want)) continue; if (embeds(s.id).includes(iface.id)) continue; // declared by embedding it const own = ownMethods(s.id); if (own !== have && !covers(own, want)) throughEmbedding.push(s); @@ -1160,10 +1161,16 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield }; // Concrete-side kinds vary by language: `class` covers Java / Kotlin / // C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value - // types that conform to protocols. Iterate both. + // types that conform to protocols. Iterate both. A Go defined type + // (`type HandlerFunc func(…)`) holds its methods as a struct does, as a + // `type_alias`; that kind is walked for Go alone. const concreteKinds = ['class', 'struct', 'union'] as const; - for (const kind of concreteKinds) { - for (const cls of queries.iterateNodesByKind(kind)) { + const concrete = [ + ...concreteKinds.map((kind) => () => queries.iterateNodesByKind(kind)), + () => queries.iterateNodesByKindIn('type_alias', ['go']), + ]; + for (const nodesOfKind of concrete) { + for (const cls of nodesOfKind()) { if ((++scanned255 & 63) === 0) await onYield(); // A class can only emit here if it HAS a supertype edge — check that // (one edge query) before materializing its methods: most classes in a