fix(go): satisfying an interface counts the methods embedding brings in (#2402)

* fix(go): satisfying an interface counts the methods embedding brings in

goImplementsEdges (#584) built each interface's method set and each struct's
from their own `contains` edges only. Embedded interfaces are `extends` edges
since #2397, and embedded structs `extends` (or `implements` for an embedded
interface), so both sides now follow those declared edges, recursively and
around cycles: an interface asks for the methods of the interfaces it embeds,
and a struct has the methods promoted from the structs, interfaces and
defined types it embeds. Synthesized edges are never followed.

A struct that embeds the interface keeps its declared edge rather than gaining
a synthesized duplicate, and under the per-interface cap, structs that declare
every method themselves are linked before those that need promoted ones, so
the cap keeps the implementations interface-impl dispatch reaches. The
empty-interface skip and the cap are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(go): the embedding test's supertype list reads declared edges only

Store in that fixture now satisfies other.Closer through the Close that
*storage.Base promotes, a go-implements edge synthesized from method names.
The assertion is about how declared embeddings resolve, as the rest of the
test already treats synthesized edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-07 02:39:46 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent dd7064698c
commit fe9518357c
4 changed files with 550 additions and 30 deletions
+1
View File
@@ -69,6 +69,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList<const char*, KeyComparator> Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading.
- 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 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 `<Route {...rest}>`, 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 `<Link to="/counter">` links led nowhere. Each route links to the page its `element` renders, past a guard like `<ProtectedRoute>` or `<AdminWrapper>`, 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.
+407
View File
@@ -0,0 +1,407 @@
/**
* Go's implicit interface satisfaction (#584) compares method sets, and in Go
* both sides include what embedding brings in:
*
* type AuthReadTx interface { // etcd's server/auth/store.go
* RLock()
* RUnlock()
* UnsafeAuthReader // UnsafeReadAuthEnabled, UnsafeGetUser, …
* }
*
* type blockQuerier struct { // Select of its own; LabelValues, LabelNames
* *storage.BaseQuerier // and Close promoted from BaseQuerier
* }
*
* goImplementsEdges read each type's own methods only. AuthReadTx asked for
* RLock and RUnlock, so etcd's RWMutex "implemented" it, and an interface
* that only embeds others (`ChunkQuerier { LabelQuerier }`) asked for nothing
* and was skipped. Embedded interfaces are `extends` edges now, embedded
* structs `extends` and embedded interfaces of a struct `implements`, so both
* method sets follow those declared edges, however deep and around cycles.
* Reading only the interface side would have dropped every struct that
* satisfies an interface through promoted methods, like blockQuerier.
*/
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 FLEET_UNITS = 45;
const FILES: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
// prometheus's storage interfaces.
'storage/storage.go': `package storage
type LabelQuerier interface {
LabelValues(name string) []string
LabelNames() []string
Close() error
}
type Querier interface {
LabelQuerier
Select(sorted bool) int
}
// No method of its own.
type ChunkQuerier interface {
LabelQuerier
}
// Promotes its methods into every struct that embeds it.
type BaseQuerier struct{}
func (b *BaseQuerier) LabelValues(name string) []string { return nil }
func (b *BaseQuerier) LabelNames() []string { return nil }
func (b *BaseQuerier) Close() error { return nil }
type Appender interface {
Append(ref uint64, v float64) (uint64, error)
Commit() error
Rollback() error
}
type Committer interface {
Commit() error
Rollback() error
}
`,
'tsdb/querier.go': `package tsdb
import "example.com/app/storage"
// A storage.Querier through BaseQuerier's promoted methods and its own Select.
type blockQuerier struct {
*storage.BaseQuerier
mint, maxt int64
}
func (q *blockQuerier) Select(sorted bool) int { return 0 }
// Select and nothing else: no Querier.
type exemplarQuerier struct{}
func (e *exemplarQuerier) Select(sorted bool) int { return 0 }
// Embeds the interface it wraps, so it already declares that it is one.
type limitAppender struct {
storage.Appender
limit int
}
func (a *limitAppender) Append(ref uint64, v float64) (uint64, error) {
return a.Appender.Append(ref, v)
}
`,
// etcd's server/auth/store.go.
'auth/store.go': `package auth
type UnsafeAuthReader interface {
UnsafeReadAuthEnabled() bool
UnsafeGetUser(name string) string
}
type AuthReadTx interface {
RLock()
RUnlock()
UnsafeAuthReader
}
// A lock: no auth reader.
type RWMutex struct{}
func (m *RWMutex) Lock() {}
func (m *RWMutex) Unlock() {}
func (m *RWMutex) RLock() {}
func (m *RWMutex) RUnlock() {}
type authReadTx struct{}
func (t *authReadTx) RLock() {}
func (t *authReadTx) RUnlock() {}
func (t *authReadTx) UnsafeReadAuthEnabled() bool { return false }
func (t *authReadTx) UnsafeGetUser(name string) string { return name }
`,
// gin's router: Engine overrides Use and gets the rest from RouterGroup.
'gin/gin.go': `package gin
type HandlerFunc func()
type HandlersChain []HandlerFunc
func (c HandlersChain) Last() HandlerFunc { return nil }
type IRoutes interface {
Use(...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
}
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
type RouterGroup struct{ Handlers HandlersChain }
func (g *RouterGroup) Use(...HandlerFunc) IRoutes { return g }
func (g *RouterGroup) GET(string, ...HandlerFunc) IRoutes { return g }
func (g *RouterGroup) Group(string, ...HandlerFunc) *RouterGroup { return g }
type Engine struct {
RouterGroup
trees []string
}
func (e *Engine) Use(middleware ...HandlerFunc) IRoutes {
e.RouterGroup.Use(middleware...)
return e
}
type Chain interface {
Last() HandlerFunc
Reset()
}
// Last is promoted from the defined type it embeds.
type routeInfo struct {
HandlersChain
path string
}
func (r *routeInfo) Reset() {}
`,
'cycles/cycles.go': `package cycles
type Walker interface{ Walk() }
// A struct may embed a pointer to itself.
type Node struct {
*Node
next *Node
}
func (n *Node) Walk() {}
type Peer interface {
Send()
Reply()
}
// Each embeds the other through a pointer, so each has both methods.
type Client struct{ *Server }
type Server struct{ *Client }
func (c *Client) Send() {}
func (s *Server) Reply() {}
// Not valid Go, but a graph can hold it: each embeds the other.
type Left interface {
Right
L()
}
type Right interface {
Left
R()
}
type both struct{}
func (both) L() {}
func (both) R() {}
`,
'empty/empty.go': `package empty
import "io"
type Any interface{}
// Embeds only what the project does not declare: nothing to match.
type Closer interface{ io.Closer }
`,
// More implementers than the cap: every unit through the base they all
// embed, and heart, declared after them, with a Beat of its own.
'fleet/fleet.go': [
'package fleet',
'',
'type Beater interface{ Beat() }',
'',
'type Base struct{}',
'',
'func (Base) Beat() {}',
'',
...Array.from({ length: FLEET_UNITS }, (_, i) => `type unit${String(i).padStart(2, '0')} struct{ Base }`),
'',
].join('\n'),
'fleet/heart.go': 'package fleet\n\ntype heart struct{}\n\nfunc (heart) Beat() {}\n',
};
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-implements-'));
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<string, unknown> => (e.metadata ?? {}) as Record<string, unknown>;
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 synthesized struct → interface edge, as `pkg.Struct -> pkg.Interface`. */
function goImplements(): string[] {
const structs = cg.getNodesByKind('struct').filter((n) => n.language === 'go');
return cg
.getOutgoingEdgesFrom(structs.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('fleet.'))
.sort();
}
describe('Go implicit satisfaction counts embedded methods on both sides', () => {
it('links exactly the structs whose method set, embeddings included, covers the interface', () => {
expect(goImplements()).toEqual([
'auth.authReadTx -> auth.AuthReadTx',
'auth.authReadTx -> auth.UnsafeAuthReader',
'cycles.Client -> cycles.Peer',
'cycles.Node -> cycles.Walker',
'cycles.Server -> cycles.Peer',
'cycles.both -> cycles.Left',
'cycles.both -> cycles.Right',
'gin.Engine -> gin.IRouter',
'gin.Engine -> gin.IRoutes',
'gin.RouterGroup -> gin.IRouter',
'gin.RouterGroup -> gin.IRoutes',
'gin.routeInfo -> gin.Chain',
'storage.BaseQuerier -> storage.ChunkQuerier',
'storage.BaseQuerier -> storage.LabelQuerier',
'tsdb.blockQuerier -> storage.ChunkQuerier',
'tsdb.blockQuerier -> storage.LabelQuerier',
'tsdb.blockQuerier -> storage.Querier',
'tsdb.limitAppender -> storage.Committer',
]);
});
it('asks for an embedded interface\'s methods too, so a lock is no AuthReadTx', () => {
const implementers = (name: string, file: string) =>
(buildTypeHierarchy(cg, one(name, file))?.descendants ?? []).map((d) => `${d.relation} ${pkg(d.node)}`).sort();
expect(implementers('AuthReadTx', 'auth/store.go')).toEqual(['implements auth.authReadTx']);
expect(implementers('Querier', 'storage/storage.go')).toEqual(['implements tsdb.blockQuerier']);
});
it('keeps a struct that embeds the interface to its declared edge', () => {
const edges = cg
.getOutgoingEdgesFrom([one('limitAppender', 'tsdb/querier.go').id], ['implements', 'extends'])
.map((e) => `${e.kind} ${pkg(cg.getNode(e.target))}${e.provenance === 'heuristic' ? ' (synthesized)' : ''}`)
.sort();
expect(edges).toEqual(['implements storage.Appender', 'implements storage.Committer (synthesized)']);
});
it('bridges a call through the interface to an override on the embedding struct', () => {
const use = cg
.getOutgoingEdgesFrom([one('IRoutes', 'gin/gin.go').id], ['contains'])
.map((e) => cg.getNode(e.target))
.find((n) => n?.name === 'Use')!;
const targets = cg
.getOutgoingEdgesFrom([use.id], ['calls'])
.filter((e) => meta(e).synthesizedBy === 'interface-impl')
.map((e) => cg.getNode(e.target)?.qualifiedName)
.sort();
// RouterGroup was linked before. Engine satisfies IRoutes only with the
// GET that RouterGroup promotes into it.
expect(targets).toEqual(['Engine::Use', 'RouterGroup::Use']);
});
it('skips an interface it knows no method of', () => {
const into = (name: string, file: string) =>
cg.getIncomingEdgesTo([one(name, file).id], ['implements']).filter((e) => meta(e).synthesizedBy === 'go-implements');
expect(into('Any', 'empty/empty.go')).toEqual([]);
expect(into('Closer', 'empty/empty.go')).toEqual([]);
});
it('caps an interface at 40 implementers, keeping those that declare its methods', () => {
const beater = one('Beater', 'fleet/fleet.go');
const linked = cg
.getIncomingEdgesTo([beater.id], ['implements'])
.filter((e) => meta(e).synthesizedBy === 'go-implements')
.map((e) => cg.getNode(e.source)!.name);
// Base, heart and 45 units satisfy Beater. heart is declared after the
// units, but a call through Beater reaches its Beat, not theirs.
expect(linked).toHaveLength(40);
expect(linked).toEqual(expect.arrayContaining(['Base', 'heart']));
expect(linked.filter((n) => n.startsWith('unit'))).toHaveLength(38);
const beat = cg
.getOutgoingEdgesFrom([beater.id], ['contains'])
.map((e) => cg.getNode(e.target))
.find((n) => n?.name === 'Beat')!;
expect(
cg
.getOutgoingEdgesFrom([beat.id], ['calls'])
.filter((e) => meta(e).synthesizedBy === 'interface-impl')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort()
).toEqual(['Base::Beat', 'heart::Beat']);
});
});
describe('a sync that edits only the embedded type', () => {
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 struct embedding it satisfies', async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-implements-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 base = 'package base\n\ntype Base struct{}\n\nfunc (Base) Close() error { return nil }\n';
write('go.mod', 'module example.com/app\n\ngo 1.22\n');
write('api/api.go', 'package api\n\ntype NamedCloser interface {\n\tClose() error\n\tName() string\n}\n');
write('base/base.go', base);
write('wrap/wrap.go', 'package wrap\n\nimport "example.com/app/base"\n\ntype Wrapper struct{ base.Base }\n\nfunc (Wrapper) Name() string { return "" }\n');
graph = await CodeGraph.init(dir, { index: true });
const g = graph;
const linked = () =>
g
.getOutgoingEdgesFrom(g.getNodesByKind('struct').map((n) => n.id), ['implements'])
.filter((e) => meta(e).synthesizedBy === 'go-implements')
.map((e) => `${g.getNode(e.source)?.name} -> ${g.getNode(e.target)?.name}`);
// Close comes from Base, in a file neither Wrapper's nor the interface's.
expect(linked()).toEqual(['Wrapper -> NamedCloser']);
write('base/base.go', base.replace('Close()', 'Shutdown()'));
await g.sync({ paths: ['base/base.go'] });
expect(linked()).toEqual([]);
write('base/base.go', base);
await g.sync({ paths: ['base/base.go'] });
expect(linked()).toEqual(['Wrapper -> NamedCloser']);
}, 60_000);
});
+3
View File
@@ -372,8 +372,11 @@ describe('the type hierarchy of an indexed Go module', () => {
one('Expr', 'parser/ast.go'),
one('exprWrapper', 'parser/ast.go'),
];
// Declared edges only: Store also satisfies other.Closer with the Close
// that storage.Base promotes, which go-implements synthesizes from names.
const supertypes = graph
.getOutgoingEdgesFrom(sources.map((n) => n.id), ['extends', 'implements'])
.filter((e) => e.provenance !== 'heuristic')
.map((e) => `${graph.getNode(e.source)?.name} ${e.kind} ${where(graph.getNode(e.target))}`)
.sort();
// An embedded interface of a struct resolves as `implements`; one of an
+139 -30
View File
@@ -810,51 +810,143 @@ const IFACE_OVERRIDE_LANGS = new Set([
* bridge ({@link interfaceOverrideEdges}, now 'go'-enabled) can link an interface
* method call to the concrete overrides.
*
* Name-only matching (signatures ignored) — 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.
* 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
* `RUnlock` plus all of `UnsafeAuthReader`, so an `RWMutex` is not one. A struct
* has the methods promoted from the structs, interfaces and defined types it
* embeds, at any depth: counting embedded interfaces alone would drop every
* struct that satisfies one through promoted methods. Synthesized edges are
* never followed (this pass writes them), and an embedding that resolved to
* nothing (`io.Closer`) adds nothing.
*
* Name-only matching (signatures ignored, and a promoted name counted even
* 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
* already is.
*/
async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
let scanned255 = 0;
const edges: Edge[] = [];
const seen = new Set<string>();
const methodNameSet = (id: string): Set<string> =>
new Set(
queries
.getOutgoingEdges(id, ['contains'])
.map((e) => queries.getNodeById(e.target))
.filter((n): n is Node => !!n && n.kind === 'method')
.map((n) => n.name),
);
// Materializes GO structs 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).
// 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'])) {
if ((++scanned255 & 63) === 0) await onYield();
goStructs.push(s);
}
const structMethods = new Map<string, Set<string>>();
for (const s of goStructs) structMethods.set(s.id, methodNameSet(s.id));
for (const iface of queries.iterateNodesByKindIn('interface', ['go'])) {
const goInterfaces: Node[] = [];
for (const i of queries.iterateNodesByKindIn('interface', ['go'])) {
if ((++scanned255 & 63) === 0) await onYield();
const want = methodNameSet(iface.id);
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<string, NodeKind | null>();
for (const s of goStructs) typeKind.set(s.id, 'struct');
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<string, Set<string>>();
const ownMethods = (id: string): Set<string> => {
let names = ownMemo.get(id);
if (!names) {
names = new Set(
queries
.getOutgoingEdges(id, ['contains'])
.map((e) => queries.getNodeById(e.target))
.filter((n): n is Node => !!n && n.kind === 'method')
.map((n) => n.name),
);
ownMemo.set(id, names);
}
return names;
};
// 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);
const NO_EMBEDS: string[] = [];
const embedMemo = new Map<string, string[]>();
const typeIds = [...typeKind.keys()];
for (let i = 0; i < typeIds.length; i += 2000) {
for (const e of queries.getOutgoingEdgesFrom(typeIds.slice(i, i + 2000), ['extends', 'implements'])) {
if (!isEmbedding(e)) continue;
const targets = embedMemo.get(e.source);
if (targets) targets.push(e.target);
else embedMemo.set(e.source, [e.target]);
}
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;
};
// 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.
const methodSet = (id: string): Set<string> => {
if (embeds(id).length === 0) return ownMethods(id);
const names = new Set<string>();
const reached = new Set<string>([id]);
const pending = [id];
while (pending.length > 0) {
const at = pending.pop()!;
for (const m of ownMethods(at)) names.add(m);
for (const t of embeds(at)) {
if (reached.has(t)) continue;
reached.add(t);
pending.push(t);
}
}
return names;
};
const covers = (have: Set<string>, want: Set<string>): boolean => {
if (have.size < want.size) return false;
for (const m of want) {
if (!have.has(m)) return false;
}
return true;
};
const structMethods = new Map<string, Set<string>>();
for (const s of goStructs) {
if ((++scanned255 & 63) === 0) await onYield();
structMethods.set(s.id, methodSet(s.id));
}
for (const iface of goInterfaces) {
if ((++scanned255 & 63) === 0) await onYield();
const want = methodSet(iface.id);
if (want.size === 0) continue; // empty interface (`any`) — would match everything
let added = 0;
for (const s of goStructs) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
const have = structMethods.get(s.id);
if (!have || have.size < want.size) continue;
let all = true;
for (const m of want) {
if (!have.has(m)) { all = false; break; }
}
if (!all) continue;
const link = (s: Node): void => {
const key = `${s.id}>${iface.id}`;
if (seen.has(key)) continue;
if (seen.has(key)) return;
seen.add(key);
edges.push({
source: s.id,
@@ -865,6 +957,23 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr
metadata: { synthesizedBy: 'go-implements', via: iface.name, registeredAt: `${s.filePath}:${s.startLine}` },
});
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.
const throughEmbedding: Node[] = [];
for (const s of goStructs) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
const have = structMethods.get(s.id);
if (!have || !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);
else link(s);
}
for (const s of throughEmbedding) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
link(s);
}
}
return edges;