diff --git a/CHANGELOG.md b/CHANGELOG.md index eb27eddf..660d92dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -168,6 +168,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, a method an interface declares, like etcd's `KV.Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)`, now links to the types its parameters and results name, so impact on `PutResponse` or `OpOption` reaches the interface method and every caller that goes through the interface. Before, an interface's methods linked to no types at all. Re-index Go projects after upgrading. - In Go, a package imported under an explicit name, like `v3 "go.etcd.io/etcd/client/v3"`, now owns that name even when another import's path ends in it, like `"github.com/cheggaaa/pb/v3"` (package `pb`) in etcd's benchmark tool. Before, whichever of the two came first took every `v3.` name, so `v3.Client` and `v3.Op` could link to nothing. Re-index Go projects after upgrading. - In Go, a struct or defined type now counts as implementing an interface only when its methods take as many parameters and return as many results as the interface's. Before, matching method names was enough, so etcd's watch cache, whose `Get` takes a context, a key and options, was listed as a `peerGetter`, whose `Get` takes a peer ID, and a call through an interface could be shown reaching methods it can never reach. Look-alikes could also crowd out the real implementations, so `codegraph_explore` stopped at grpc-go's xDS `TransportBuilder` instead of reaching the transport it builds. A server still counts for the gRPC client interface of the service it serves, like etcd's `kvServer` for `KVClient`, so a client's call keeps leading to the handler that serves it. Re-index Go projects after upgrading. +- In Go, an interface with an unexported method, like the `refresher { refresh(…) }` that several of prometheus's service-discovery packages each declare, now counts as implemented only by types whose method of that name is declared in the interface's own package, since Go accepts no other. Before, any type in any package with a method of that name was listed, so a call through hetzner's `refresher`, which `codegraph_explore` follows from `Discovery.Run`, was shown reaching every provider's `refresh` instead of only hetzner's own. A type that gets the method from a type it embeds still counts, like etcd's gRPC servers, which get `mustEmbedUnimplementedKVServer` from the generated stub they embed, and a call through the interface reaches that embedded method, not a same-named one the type declares for itself. 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 `.jsx` files, the components and functions that read a constant or variable declared at the top of the file, like a page loaded with `const LoginPage = Loadable(lazy(…))` and rendered as ``, a route table, or a messages or style object, now show up in its impact, as they already did in `.js` and `.tsx` files. Before, `.jsx` files were left out, so `codegraph impact` and the impact trail in `codegraph_explore` and `codegraph_node` showed nothing depending on such a value. Re-index projects with `.jsx` files after upgrading. diff --git a/__tests__/go-implements-unexported.test.ts b/__tests__/go-implements-unexported.test.ts new file mode 100644 index 00000000..c68798b7 --- /dev/null +++ b/__tests__/go-implements-unexported.test.ts @@ -0,0 +1,321 @@ +/** + * In Go an unexported method name belongs to the package that declares it. + * prometheus has a `refresher` interface in each of several discovery + * packages: + * + * type refresher interface { // discovery/hetzner + * refresh(context.Context) ([]*targetgroup.Group, error) + * } + * + * Only a type of package hetzner can have that `refresh`: aws's EC2Discovery + * declares a `refresh` of its own, but it is package aws's method, and Go does + * not let it satisfy hetzner's interface. goImplementsEdges matched method + * names only, so it linked each refresher to every provider in every package. + * + * Where the method is declared is what counts, not where the type is. etcd's + * gRPC servers live outside api/etcdserverpb, yet they satisfy its + * `KVServer { …; mustEmbedUnimplementedKVServer() }`: they embed the + * generated `UnsafeKVServer` interface or `UnimplementedKVServer` struct, and + * the method those bring is declared in the interface's package. + */ +import { describe, it, expect, beforeAll, afterAll } 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'; + +// Base and 39 units: exactly the cap. +const POOL_UNITS = 39; +const unit = (i: number) => `unit${String(i).padStart(2, '0')}`; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // prometheus's discovery/refresh: every provider embeds its Discovery. + 'discovery/refresh/refresh.go': `package refresh + +type Discovery struct { + refreshf func() ([]string, error) +} + +func (d *Discovery) Run() {} + +func (d *Discovery) refresh() ([]string, error) { return d.refreshf() } +`, + 'discovery/hetzner/hetzner.go': `package hetzner + +import "example.com/app/discovery/refresh" + +type refresher interface { + refresh() ([]string, error) +} + +// A refresh of its own, declared in package hetzner: a refresher. +type hcloudDiscovery struct { + *refresh.Discovery +} + +func (d *hcloudDiscovery) refresh() ([]string, error) { return nil, nil } + +// A defined type of the package is one too. +type robotTargets []string + +func (r robotTargets) refresh() ([]string, error) { return r, nil } + +// Its only refresh is the one package refresh declares on the Discovery it +// embeds: no refresher, for all that it sits in this package. +type Discovery struct { + *refresh.Discovery +} +`, + 'discovery/aws/ec2.go': `package aws + +import "example.com/app/discovery/refresh" + +// A refresh of its own, but package aws's: no hetzner refresher. +type EC2Discovery struct { + *refresh.Discovery +} + +func (d *EC2Discovery) refresh() ([]string, error) { return nil, nil } + +// Nor is a defined type of package aws. +type staticTargets []string + +func (s staticTargets) refresh() ([]string, error) { return s, nil } +`, + // etcd's api/etcdserverpb, as protoc-gen-go-grpc writes it. + 'api/pb/rpc_grpc.pb.go': `package pb + +type KVServer interface { + Range(key string) (string, error) + Put(key string) (string, error) + mustEmbedUnimplementedKVServer() +} + +type UnimplementedKVServer struct{} + +func (UnimplementedKVServer) Range(key string) (string, error) { return "", nil } +func (UnimplementedKVServer) Put(key string) (string, error) { return "", nil } +func (UnimplementedKVServer) mustEmbedUnimplementedKVServer() {} + +type UnsafeKVServer interface { + mustEmbedUnimplementedKVServer() +} +`, + 'server/v3rpc/key.go': `package v3rpc + +import pb "example.com/app/api/pb" + +// etcd's kvServer: the stub interface brings pb's mustEmbedUnimplementedKVServer. +type kvServer struct { + pb.UnsafeKVServer +} + +func (s *kvServer) Range(key string) (string, error) { return "", nil } +func (s *kvServer) Put(key string) (string, error) { return "", nil } + +// The stub struct brings it, and Put. +type kvProxy struct { + pb.UnimplementedKVServer +} + +func (p *kvProxy) Range(key string) (string, error) { return "", nil } + +// Its mustEmbedUnimplementedKVServer is package v3rpc's: no KVServer. +type fakeKV struct{} + +func (fakeKV) Range(key string) (string, error) { return "", nil } +func (fakeKV) Put(key string) (string, error) { return "", nil } +func (fakeKV) mustEmbedUnimplementedKVServer() {} +`, + // An interface of package v3rpc asks for pb's method through the interface + // it embeds. + 'server/v3rpc/closable.go': `package v3rpc + +import pb "example.com/app/api/pb" + +type closableKV interface { + pb.KVServer + Close() error +} + +func (s *kvServer) Close() error { return nil } + +func (fakeKV) Close() error { return nil } +`, + 'lease/lease.go': `package lease + +type checkpointer interface { + checkpoint() error + Expired() bool +} + +// A checkpoint and no Expired: neither is a checkpointer itself. +type Lessor struct{} + +func (l *Lessor) checkpoint() error { return nil } + +type Store struct{} + +func (s *Store) checkpoint() error { return nil } +`, + 'lease/proxy/proxy.go': `package proxy + +import "example.com/app/lease" + +// Its own checkpoint is package proxy's. A call through a checkpointer runs +// the one the Lessor it embeds declares in package lease. +type leaseProxy struct { + *lease.Lessor +} + +func (p *leaseProxy) checkpoint() error { return nil } +func (p *leaseProxy) Expired() bool { return false } + +// local's checkpoint, one level down, is package proxy's too. It hides +// nothing: Store's, two levels down, is the checkpointer's. +type local struct{} + +func (local) checkpoint() error { return nil } + +type wrapped struct{ *lease.Store } + +type layered struct { + local + wrapped +} + +func (layered) Expired() bool { return false } +`, + // More pingers than the cap. early comes first in file order, but its own + // ping is package a's and pool's comes from the Base it embeds, so it waits + // behind the 40 types that declare pool's ping. + 'a/early.go': 'package a\n\nimport "example.com/app/pool"\n\ntype early struct{ pool.Base }\n\nfunc (early) ping() {}\n', + 'pool/pool.go': [ + 'package pool', + '', + 'type pinger interface{ ping() }', + '', + 'type Base struct{}', + '', + 'func (Base) ping() {}', + '', + ...Array.from({ length: POOL_UNITS }, (_, i) => `type ${unit(i)} struct{}\n\nfunc (${unit(i)}) ping() {}\n`), + ].join('\n'), +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-unexported-')); + 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}`; + +/** `file:line` of the first fixture line containing `text`. */ +function at(file: string, text: string): string { + const line = FILES[file]!.split('\n').findIndex((l) => l.includes(text)); + expect(line, `${text} in ${file}`).toBeGreaterThanOrEqual(0); + return `${file}:${line + 1}`; +} + +/** 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]!; +} + +/** The synthesized edges into an interface, as `pkg.Type`. */ +function implementers(iface: Node): string[] { + return cg + .getIncomingEdgesTo([iface.id], ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => pkg(cg.getNode(e.source))) + .sort(); +} + +/** Where a call through an interface method dispatches, as `Target::m` or `Target::m <- promotedInto @registeredAt`. */ +function dispatch(iface: Node, name: string): string[] { + const method = cg + .getOutgoingEdgesFrom([iface.id], ['contains']) + .map((e) => cg.getNode(e.target)) + .find((n) => n?.name === name); + expect(method, `${iface.name}.${name}`).toBeTruthy(); + return cg + .getOutgoingEdgesFrom([method!.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => { + const target = cg.getNode(e.target)?.qualifiedName; + const into = meta(e).promotedInto; + return into === undefined ? `${target}` : `${target} <- ${into} @${meta(e).registeredAt}`; + }) + .sort(); +} + +describe('an unexported interface method is satisfied only from its own package', () => { + it('links no type of another package that declares a method of that name', () => { + const refresher = one('refresher', 'discovery/hetzner/hetzner.go'); + // Not aws's EC2Discovery or staticTargets, nor refresh.Discovery, nor + // hetzner's Discovery, whose refresh is package refresh's. + expect(implementers(refresher)).toEqual(['discovery/hetzner.hcloudDiscovery', 'discovery/hetzner.robotTargets']); + const hierarchy = (buildTypeHierarchy(cg, refresher)?.descendants ?? []).map((d) => pkg(d.node)).sort(); + expect(hierarchy).toEqual(['discovery/hetzner.hcloudDiscovery', 'discovery/hetzner.robotTargets']); + }); + + it('links a gRPC server through the stub it embeds from the interface\'s package', () => { + // kvServer embeds the UnsafeKVServer interface, kvProxy the + // UnimplementedKVServer struct. fakeKV declares the method itself, in + // package v3rpc. + expect(implementers(one('KVServer', 'api/pb/rpc_grpc.pb.go'))).toEqual([ + 'api/pb.UnimplementedKVServer', + 'server/v3rpc.kvProxy', + 'server/v3rpc.kvServer', + ]); + // kvServer declares that one by embedding it. + expect(implementers(one('UnsafeKVServer', 'api/pb/rpc_grpc.pb.go'))).toEqual([ + 'api/pb.UnimplementedKVServer', + 'server/v3rpc.kvProxy', + ]); + }); + + it('asks for an embedded interface\'s method in that interface\'s package', () => { + expect(implementers(one('closableKV', 'server/v3rpc/closable.go'))).toEqual(['server/v3rpc.kvServer']); + }); + + it('dispatches a call through the interface to the method of its own package', () => { + const checkpointer = one('checkpointer', 'lease/lease.go'); + expect(implementers(checkpointer)).toEqual(['lease/proxy.layered', 'lease/proxy.leaseProxy']); + // Not leaseProxy's own checkpoint, nor local's at the shallower depth. + expect(dispatch(checkpointer, 'checkpoint')).toEqual([ + `Lessor::checkpoint <- leaseProxy @${at('lease/proxy/proxy.go', '\t*lease.Lessor')}`, + `Store::checkpoint <- layered @${at('lease/proxy/proxy.go', '\twrapped')}`, + ]); + expect(dispatch(checkpointer, 'Expired')).toEqual(['layered::Expired', 'leaseProxy::Expired']); + // A gRPC server's own mustEmbedUnimplementedKVServer is no override. + expect(dispatch(one('KVServer', 'api/pb/rpc_grpc.pb.go'), 'mustEmbedUnimplementedKVServer')).toEqual([ + 'UnimplementedKVServer::mustEmbedUnimplementedKVServer', + ]); + }); + + it('fills the cap with the types that declare the method in its package first', () => { + const pinger = one('pinger', 'pool/pool.go'); + expect(implementers(pinger)).toEqual( + ['pool.Base', ...Array.from({ length: POOL_UNITS }, (_, i) => `pool.${unit(i)}`)].sort() + ); + }); +}); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index 355cb329..f54ce3c4 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -828,6 +828,21 @@ function isGoEmbedding(e: Edge, kindOf: (id: string) => NodeKind | null): boolea const to = kindOf(e.target); return to !== null && (kindOf(e.source) !== 'interface' || to === 'interface'); } +/** An exported Go identifier starts with an upper-case letter, in Unicode's sense. */ +const GO_EXPORTED_NAME = /^\p{Lu}/u; +/** + * What a Go method matches an interface's method by: its name, and for an + * unexported name the package that declares it as well. Go makes the + * `refresh` declared in discovery/hetzner and the one declared in + * discovery/aws two different methods, so neither satisfies the other's + * interface. A package is a directory, the method's own file's, so a method + * that embedding promotes keeps the package of the type declaring it. + */ +function goMethodKey(m: Node): string { + if (GO_EXPORTED_NAME.test(m.name)) return m.name; + const file = m.filePath.replace(/\\/g, '/'); + return `${file.slice(0, Math.max(file.lastIndexOf('/'), 0))}\0${m.name}`; +} /** * The top-level items of the Go bracketed list opening at `text[open]` — the * parameters of `(ctx context.Context, a, b int)` are `ctx context.Context`, `a` @@ -946,6 +961,15 @@ function goGrpcServerArity(signature: string | undefined): string | null { * never followed (this pass writes them), and an embedding that resolved to * nothing (`io.Closer`) adds nothing. * + * Methods match by name, and an unexported name by its package too + * ({@link goMethodKey}). prometheus has a `refresher { refresh(…) }` in each of + * several discovery packages, and only a type with a `refresh` declared in that + * package satisfies it, its own or one embedding brings in: aws's + * EC2Discovery, with a `refresh` of package aws, is no hetzner refresher. etcd's + * gRPC servers sit outside the generated package and still satisfy its + * `KVServer`, whose `mustEmbedUnimplementedKVServer` the `UnsafeKVServer` or + * `UnimplementedKVServer` they embed declares there. + * * Each wanted method also needs a declaration with its parameter and result * counts, read off the stored signatures: Go wants the signatures identical, * and the counts are what the text settles for certain — a cache's `Get(ctx, @@ -988,8 +1012,10 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr for (const i of goInterfaces) typeKind.set(i.id, 'interface'); const kindOf = (id: string): NodeKind | null => typeKind.get(id) ?? null; - // Memoized: an embedded base is read once, however many types embed it. Each - // method's stored signature rides along for the arity check below. + // A type's own methods, each by its goMethodKey, so every method set below + // holds an unexported name with its package. Each method's stored signature + // rides along under the same key for the arity check below. Memoized: an + // embedded base is read once, however many types embed it. const ownMemo = new Map>(); const ownSignatures = new Map>(); const ownMethods = (id: string): Set => { @@ -1000,10 +1026,11 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr for (const e of queries.getOutgoingEdges(id, ['contains'])) { const n = queries.getNodeById(e.target); if (!n || n.kind !== 'method') continue; - names.add(n.name); - const list = signatures.get(n.name); + const key = goMethodKey(n); + names.add(key); + const list = signatures.get(key); if (list) list.push(n.signature); - else signatures.set(n.name, [n.signature]); + else signatures.set(key, [n.signature]); } ownMemo.set(id, names); ownSignatures.set(id, signatures); @@ -1063,10 +1090,10 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr return true; }; - // Each method name in a type's method set → the arities (`params/results`) - // its declarations there have, or null when one of their signatures doesn't - // read (then the name rules nothing out). Built only for the pairs whose - // names already match. + // Each method in a type's method set, by its goMethodKey, → the arities + // (`params/results`) its declarations there have, or null when one of their + // signatures doesn't read (then the name rules nothing out). Built only for + // the pairs whose names already match. const arityMemo = new Map | null>>(); const methodArities = (id: string): Map | null> => { let arities = arityMemo.get(id); @@ -1166,7 +1193,9 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr }; // 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. + // the interface-dispatch bridge links a call through the interface to. A + // namesake it declares of an unexported method from another package is + // not one of them: the method it needs is promoted. const throughEmbedding: Node[] = []; let wanted: Map | null> | undefined; for (const s of candidates) { @@ -1330,27 +1359,29 @@ async function kotlinExpectActualEdges(queries: QueryBuilder, onYield: MaybeYiel return edges; } -/** The methods a Go struct runs for a name it does not declare, and the line of the embedding they come through. */ +/** The methods a Go struct runs for a method it does not declare, and the line of the embedding they come through. */ interface GoPromotion { methods: Node[]; line: number; } /** - * Which embedded type's method a Go struct runs for a name it does not + * Which embedded type's method a Go struct runs for a method it does not * declare, picked the way Go's selector picks it: breadth-first through the - * struct's embeddings, the shallowest depth holding the name wins, and the - * name selects nothing when it occurs more than once at that depth, one type + * struct's embeddings, the shallowest depth holding the method wins, and it + * selects nothing when it occurs more than once at that depth, one type * reached along two paths included. A type met at a shallower depth is not * walked again. An embedded interface brings its whole method set at its own * depth, and a method it provides is a dynamic call once more, so that is no * promotion either. Struct fields are not in the graph: a field that hides a - * promoted method is not seen. + * promoted method is not seen. The method is asked for by its + * {@link goMethodKey}: a namesake of an unexported method declared in another + * package is another method, so it neither provides nor hides it. */ function goPromotions( queries: QueryBuilder, methodsOf: (id: string) => Node[] -): (structId: string, name: string) => GoPromotion | null { +): (structId: string, key: string) => GoPromotion | null { const kinds = new Map(); const kindOf = (id: string): NodeKind | null => { let kind = kinds.get(id); @@ -1386,7 +1417,7 @@ function goPromotions( const pending = [id]; while (pending.length > 0) { const at = pending.pop()!; - for (const m of methodsOf(at)) names.add(m.name); + for (const m of methodsOf(at)) names.add(goMethodKey(m)); for (const e of embeds(at)) { if (reached.has(e.target)) continue; reached.add(e.target); @@ -1397,13 +1428,13 @@ function goPromotions( } return names; }; - const provides = (id: string, name: string): boolean => - kindOf(id) === 'interface' ? ifaceMethods(id).has(name) : methodsOf(id).some((m) => m.name === name); + const provides = (id: string, key: string): boolean => + kindOf(id) === 'interface' ? ifaceMethods(id).has(key) : methodsOf(id).some((m) => goMethodKey(m) === key); const memo = new Map(); - return (structId, name) => { - const key = `${structId}>${name}`; - const hit = memo.get(key); + return (structId, key) => { + const memoKey = `${structId}>${key}`; + const hit = memo.get(memoKey); if (hit !== undefined) return hit; let found: GoPromotion | null = null; // Each type at the current depth, with how many paths reach it and the @@ -1425,19 +1456,19 @@ function goPromotions( let provider: string | undefined; for (const [id, at] of next) { met.add(id); - if (!provides(id, name)) continue; + if (!provides(id, key)) continue; paths += at.paths; provider = id; } if (provider !== undefined) { if (paths === 1 && kindOf(provider) !== 'interface') { - found = { methods: methodsOf(provider).filter((m) => m.name === name), line: next.get(provider)!.line }; + found = { methods: methodsOf(provider).filter((m) => goMethodKey(m) === key), line: next.get(provider)!.line }; } break; } level = next; } - memo.set(key, found); + memo.set(memoKey, found); return found; }; } @@ -1524,17 +1555,22 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield // Group impl methods by name to handle OVERLOADS: an interface `list()` and // `list(params)` are distinct nodes and a call may resolve to either, so // link every base overload → every same-name impl overload (keying by name - // alone would drop all but one and miss the resolved overload). - const implByName = new Map(); + // alone would drop all but one and miss the resolved overload). A Go + // interface's unexported method is matched in its own package only + // (goMethodKey): a namesake the type declares elsewhere is another + // method, and the one the interface asks for is promoted. + const keyOf = base.language === 'go' && base.kind === 'interface' ? goMethodKey : (m: Node): string => m.name; + const implsByKey = new Map(); for (const m of implMethods) { - const arr = implByName.get(m.name); - if (arr) arr.push(m); else implByName.set(m.name, [m]); + const key = keyOf(m); + const arr = implsByKey.get(key); + if (arr) arr.push(m); else implsByKey.set(key, [m]); } let added = 0; const unmatched: Node[] = []; for (const bm of baseMethodsOf(base)) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; - const impls = implByName.get(bm.name); + const impls = implsByKey.get(keyOf(bm)); if (!impls) { if (promotes) unmatched.push(bm); continue; @@ -1569,7 +1605,7 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield let budget = left; for (const bm of methods) { if (budget <= 0) break; - const promotion = goPromotion(cls.id, bm.name); + const promotion = goPromotion(cls.id, goMethodKey(bm)); if (!promotion) continue; for (const m of promotion.methods) { if (budget <= 0) break;