diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a188536..071dd2931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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 call through an interface now reaches a method that the implementing struct gets from a type it embeds: prometheus's `targetScraper` gets `Report` from the `*Target` it embeds, so a call through the `scraper` interface now reaches `Target.Report`, and etcd's `Hasher.MemberID` reaches `EtcdServer.MemberID` through `hasherAdapter`. Before, the call reached only structs that declare the method themselves, often just a test double, so `codegraph_explore`'s call path stopped at the interface. The method is picked the way Go picks it, from the nearest embedded type, and none is linked when two embedded types at the same depth both have it or it comes from an embedded interface. 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. - In Go, a call or type written through an import whose path ends in a version, or in something other than the package's name, is now known to go through that import: `yaml.Unmarshal(…)` after `import "go.yaml.in/yaml/v3"` or `"gopkg.in/yaml.v3"`, `sqlite3.Error` after `import "github.com/mattn/go-sqlite3"`, `klog.V(2)` after `import "k8s.io/klog/v2"`. Before, only the last part of the path named such an import (`v3`, `yaml.v3`, `go-sqlite3`), so the name was matched on its own and could link to any project function, method or type that shared it: kubernetes' `klog.V(…)` calls were linked to a logging wrapper's `V` method, and etcd's `semver.Version` parameters to an unrelated `Version` function. A name from another module now links to nothing, and one through a package of your own project, like `kit.New()` after `import "example.com/kit/v2"`, links to that package's symbol. A comment in an import block is also no longer taken for the name of the import after it. Re-index Go projects after upgrading. Thanks @danusha2345 for the comment fix. (#2374) 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 f1a49cb36..a063beff2 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -1334,14 +1334,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 7d146453a..d84861728 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -815,12 +815,21 @@ function isGoEmbedding(e: Edge, kindOf: (id: string) => NodeKind | null): boolea } /** * 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, and to the methods embedding promotes. * + * 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 @@ -835,7 +844,7 @@ function isGoEmbedding(e: Edge, kindOf: (id: string) => NodeKind | null): boolea * 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 { @@ -843,13 +852,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'])) { @@ -857,20 +868,12 @@ 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; - }; + const kindOf = (id: string): NodeKind | null => typeKind.get(id) ?? null; // Memoized: an embedded base is read once, however many types embed it. const ownMemo = new Map>(); @@ -890,8 +893,8 @@ 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. + // three. Every type's are read up front in a few batched queries rather than + // one query per type. const isEmbedding = (e: Edge): boolean => isGoEmbedding(e, kindOf); const NO_EMBEDS: string[] = []; const embedMemo = new Map(); @@ -905,15 +908,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. @@ -942,10 +937,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) { @@ -967,14 +967,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); @@ -1289,10 +1289,16 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield const promoted: { cls: Node; methods: Node[]; left: number }[] = []; // 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