mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 23:01:53 +08:00
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:
co-authored by
Claude Opus 5.5
parent
dd7064698c
commit
fe9518357c
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user