From 450785984581af931e858c77a8eb0c799f2eef0b Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 09:35:40 -0500 Subject: [PATCH 1/6] fix(go): an interface or struct embedding a standard-library type counts its methods goImplementsEdges (#584) follows declared embeddings since #2402, but an embedding of a type outside the index resolves to nothing, so it added nothing: prometheus's TBRun (testing.TB plus Run) asked only for Run and matched 40 service discoverers, an interface that only embeds standard library ones (QueryLogger: slog.Handler plus io.Closer) asked for nothing and was skipped, and the predeclared error was not recorded at all. The method names of the standard-library types projects embed are now known (go-stdlib-methods.ts, read from Go's own source), and both extractors record an embedded `error`, which stays unresolved like io.Closer. goImplementsEdges reads each type's failed `extends` rows, reads the package back from the source line, and keeps three kinds of method name apart: declared in the project, known from the standard library, and embedded types nothing describes. A standard-library name answers only what the standard library asks for (etcd's gRPC LockClient.Lock is not an embedded sync.Mutex's Lock; grpc-go's Sink.Write is not an embedded net.Conn's). An undescribed embedding is met by a struct embedding the same type, or by the one struct that has everything else the interface names (kapacitor's Diagnostic, a third-party Logger plus Log, no longer matches 40 structs with a Log). Co-Authored-By: Claude Opus 5.5 --- __tests__/go-implements-embedding.test.ts | 15 +- __tests__/go-implements-stdlib.test.ts | 441 ++++++++++++++++++++++ __tests__/go-interface-embedding.test.ts | 9 +- codegraph-kernel/src/go.rs | 7 +- src/db/queries.ts | 36 ++ src/extraction/languages/go.ts | 6 +- src/resolution/callback-synthesizer.ts | 127 +++++-- src/resolution/go-stdlib-methods.ts | 246 ++++++++++++ src/resolution/name-matcher.ts | 9 +- 9 files changed, 860 insertions(+), 36 deletions(-) create mode 100644 __tests__/go-implements-stdlib.test.ts create mode 100644 src/resolution/go-stdlib-methods.ts diff --git a/__tests__/go-implements-embedding.test.ts b/__tests__/go-implements-embedding.test.ts index 702a52f634..ca317090c8 100644 --- a/__tests__/go-implements-embedding.test.ts +++ b/__tests__/go-implements-embedding.test.ts @@ -217,11 +217,18 @@ func (both) R() {} `, 'empty/empty.go': `package empty -import "io" +import ( + "io" + + "go.uber.org/zap/zapcore" +) type Any interface{} -// Embeds only what the project does not declare: nothing to match. +// Embeds only a type nothing here names the methods of: nothing to match. +type Core interface{ zapcore.Core } + +// What io.Closer asks for: a Close. type Closer interface{ io.Closer } `, // More implementers than the cap: every unit through the base they all @@ -294,8 +301,10 @@ describe('Go implicit satisfaction counts embedded methods on both sides', () => 'gin.RouterGroup -> gin.IRouter', 'gin.RouterGroup -> gin.IRoutes', 'gin.routeInfo -> gin.Chain', + 'storage.BaseQuerier -> empty.Closer', 'storage.BaseQuerier -> storage.ChunkQuerier', 'storage.BaseQuerier -> storage.LabelQuerier', + 'tsdb.blockQuerier -> empty.Closer', 'tsdb.blockQuerier -> storage.ChunkQuerier', 'tsdb.blockQuerier -> storage.LabelQuerier', 'tsdb.blockQuerier -> storage.Querier', @@ -337,7 +346,7 @@ describe('Go implicit satisfaction counts embedded methods on both sides', () => 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([]); + expect(into('Core', 'empty/empty.go')).toEqual([]); }); it('caps an interface at 40 implementers, keeping those that declare its methods', () => { diff --git a/__tests__/go-implements-stdlib.test.ts b/__tests__/go-implements-stdlib.test.ts new file mode 100644 index 0000000000..43b6e49f75 --- /dev/null +++ b/__tests__/go-implements-stdlib.test.ts @@ -0,0 +1,441 @@ +/** + * Go's implicit interface satisfaction (#584) compares method sets, following + * the embeddings that resolve (#2402). An embedding of a type outside the + * index resolves to nothing, so it added nothing: + * + * type TBRun interface { // prometheus's promql/promqltest + * testing.TB // two dozen methods, one of them unexported + * Run(string, func(*testing.T)) bool + * } + * + * asked only for `Run`, and 40 service discoverers "implemented" it. The + * embedded `error` was not even recorded. + * + * The method names of the standard-library types projects embed are known + * now, and `error` is recorded as the embedding it is. Such a name answers + * only what the standard library asks for: an embedded `sync.Mutex` gives no + * struct etcd's gRPC `LockClient.Lock`. Embedding any other outside type + * (`zapcore.Core`, `grpc.ClientStream`) asks for methods nothing names: a + * struct embedding the same type has them, and otherwise only the one struct + * with every method the interface does name is taken to implement it. + */ +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 FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // prometheus. + 'promql/promql.go': `package promql + +import ( + "context" + "io" + "log/slog" + "os" + "testing" +) + +// All of testing.TB, which only an embedding can have, and Run. +type TBRun interface { + testing.TB + Run(string, func(*testing.T)) bool +} + +// A Run, and nothing of testing.TB. +type Discovery struct{} + +func (d *Discovery) Run(name string, f func(*testing.T)) bool { return false } + +// testing.TB through *testing.T, and a Run of its own. +type tRunner struct{ *testing.T } + +func (r tRunner) Run(name string, f func(*testing.T)) bool { return r.T.Run(name, f) } + +type syncWriter interface { + io.Writer + Sync() error +} + +// Sync, and no Write. +type scrapePool struct{} + +func (sp *scrapePool) Sync() error { return nil } + +type queryLogFile struct{} + +func (f *queryLogFile) Write(p []byte) (int, error) { return len(p), nil } +func (f *queryLogFile) Sync() error { return nil } + +type SegmentFile interface { + Stat() (os.FileInfo, error) + Sync() error + io.Writer + io.Reader + io.Closer +} + +// Write through the SegmentFile it embeds, which has it from io.Writer. +type Segment struct { + SegmentFile + dir string +} + +// No method of its own: what slog.Handler and io.Closer ask for. +type QueryLogger interface { + slog.Handler + io.Closer +} + +type JSONFileLogger struct{} + +func (l *JSONFileLogger) Enabled(ctx context.Context, level slog.Level) bool { return true } +func (l *JSONFileLogger) Handle(ctx context.Context, r slog.Record) error { return nil } +func (l *JSONFileLogger) WithAttrs(attrs []slog.Attr) slog.Handler { return l } +func (l *JSONFileLogger) WithGroup(name string) slog.Handler { return l } +func (l *JSONFileLogger) Close() error { return nil } +`, + // etcd. + 'fileutil/fileutil.go': `package fileutil + +import ( + "bufio" + "context" + "io" + "io/fs" + "os" + "sync" +) + +type FileReader interface { + io.Reader + FileInfo() (fs.FileInfo, error) +} + +// Read is promoted from *os.File. +type fileReader struct{ *os.File } + +func (fr *fileReader) FileInfo() (fs.FileInfo, error) { return fr.Stat() } + +// Read is promoted from *bufio.Reader. +type FileBufReader struct { + *bufio.Reader + fi fs.FileInfo +} + +func (fb *FileBufReader) FileInfo() (fs.FileInfo, error) { return fb.fi, nil } + +// FileInfo, and nothing to Read with. +type fileStat struct{} + +func (s *fileStat) FileInfo() (fs.FileInfo, error) { return nil, nil } + +// v3lockpb's client: Lock and Unlock are gRPC calls. +type LockClient interface { + Lock(ctx context.Context, name string) error + Unlock(ctx context.Context, key string) error +} + +// A mutex's Lock and Unlock are not those. +type waitRecorder struct { + sync.Mutex + actions []string +} + +type lockProxy struct{} + +func (p *lockProxy) Lock(ctx context.Context, name string) error { return nil } +func (p *lockProxy) Unlock(ctx context.Context, key string) error { return nil } +`, + // gin. + 'gin/response_writer.go': `package gin + +import ( + "bufio" + "net" + "net/http" +) + +type ResponseWriter interface { + http.ResponseWriter + http.Hijacker + http.Flusher + http.CloseNotifier + Status() int + Size() int + WriteString(string) (int, error) + Written() bool + WriteHeaderNow() + Pusher() http.Pusher +} + +// Header is promoted from the http.ResponseWriter it wraps. +type responseWriter struct { + http.ResponseWriter + size int + status int +} + +func (w *responseWriter) WriteHeader(code int) {} +func (w *responseWriter) WriteHeaderNow() {} +func (w *responseWriter) Write(data []byte) (int, error) { return len(data), nil } +func (w *responseWriter) WriteString(s string) (int, error) { return len(s), nil } +func (w *responseWriter) Status() int { return w.status } +func (w *responseWriter) Size() int { return w.size } +func (w *responseWriter) Written() bool { return w.size >= 0 } +func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil } +func (w *responseWriter) CloseNotify() <-chan bool { return nil } +func (w *responseWriter) Flush() {} +func (w *responseWriter) Pusher() http.Pusher { return nil } + +// The methods ResponseWriter declares, none that http asks for. +type sizeTracker struct{ size, status int } + +func (t *sizeTracker) Status() int { return t.status } +func (t *sizeTracker) Size() int { return t.size } +func (t *sizeTracker) WriteString(s string) (int, error) { return len(s), nil } +func (t *sizeTracker) Written() bool { return t.size >= 0 } +func (t *sizeTracker) WriteHeaderNow() {} +func (t *sizeTracker) Pusher() http.Pusher { return nil } +`, + 'errs/errs.go': `package errs + +type coded interface { + error + Code() int +} + +// Error is promoted from the error it wraps. +type statusErr struct { + error + code int +} + +func (e statusErr) Code() int { return e.code } + +// A Code, and no Error. +type counter struct{ n int } + +func (c *counter) Code() int { return c.n } +`, + // Interfaces embedding types nothing here describes. + 'diag/diag.go': `package diag + +import ( + "github.com/libdns/libdns" + plog "github.com/prometheus/common/log" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" +) + +// kapacitor's scraper.Diagnostic: Log, and a Logger. +type Diagnostic interface { + plog.Logger + Log(keyvals ...interface{}) error +} + +type ScraperHandler struct{} + +func (h *ScraperHandler) Debug(args ...interface{}) {} +func (h *ScraperHandler) Log(keyvals ...interface{}) error { return nil } + +type AlertNode struct{} + +func (n *AlertNode) Log(keyvals ...interface{}) error { return nil } + +// caddy's: nothing else has a FlushTo. +type LogBufferCoreInterface interface { + zapcore.Core + FlushTo(*zap.Logger) +} + +type LogBufferCore struct{} + +func (c *LogBufferCore) FlushTo(l *zap.Logger) {} + +type ClientStream interface { + grpc.ClientStream + Send(m any) error + Recv(m any) error +} + +// Has the gRPC stream's methods by embedding it. +type wrappedStream struct{ grpc.ClientStream } + +func (s *wrappedStream) Send(m any) error { return nil } +func (s *wrappedStream) Recv(m any) error { return nil } + +// Sends and receives, as a server stream. +type serverStream struct{} + +func (s *serverStream) Send(m any) error { return nil } +func (s *serverStream) Recv(m any) error { return nil } + +// Nothing it asks for has a name here. +type DNSProvider interface { + libdns.RecordGetter + libdns.RecordSetter +} +`, +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-implements-stdlib-')); + 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 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))}`) + .sort(); +} + +describe('Go implicit satisfaction knows what standard-library embeddings bring', () => { + it('links exactly the structs that have every method, embedded ones included', () => { + expect(goImplements()).toEqual([ + 'diag.LogBufferCore -> diag.LogBufferCoreInterface', + 'diag.wrappedStream -> diag.ClientStream', + 'errs.statusErr -> errs.coded', + 'fileutil.FileBufReader -> fileutil.FileReader', + 'fileutil.fileReader -> fileutil.FileReader', + 'fileutil.lockProxy -> fileutil.LockClient', + 'gin.responseWriter -> gin.ResponseWriter', + 'promql.JSONFileLogger -> promql.QueryLogger', + 'promql.Segment -> promql.syncWriter', + 'promql.queryLogFile -> promql.syncWriter', + 'promql.tRunner -> promql.TBRun', + ]); + }); + + it('asks for all of testing.TB, so a discoverer with a Run is no TBRun', () => { + const implementers = (name: string, file: string) => + (buildTypeHierarchy(cg, one(name, file))?.descendants ?? []).map((d) => `${d.relation} ${pkg(d.node)}`).sort(); + expect(implementers('TBRun', 'promql/promql.go')).toEqual(['implements promql.tRunner']); + expect(implementers('syncWriter', 'promql/promql.go')).toEqual([ + 'implements promql.Segment', + 'implements promql.queryLogFile', + ]); + // A call through TBRun reaches tRunner's Run, not the discoverer's. + const run = cg + .getOutgoingEdgesFrom([one('TBRun', 'promql/promql.go').id], ['contains']) + .map((e) => cg.getNode(e.target)) + .find((n) => n?.name === 'Run')!; + expect( + cg + .getOutgoingEdgesFrom([run.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => cg.getNode(e.target)?.qualifiedName) + ).toEqual(['tRunner::Run']); + }); + + it('records an embedded error, which stays outside the graph', () => { + for (const [name, file] of [['statusErr', 'errs/errs.go'], ['coded', 'errs/errs.go']] as const) { + const node = one(name, file); + expect( + cg.getUnresolvedReferencesFrom(node.id).filter((r) => r.referenceKind === 'extends').map((r) => r.referenceName) + ).toEqual(['error']); + expect(cg.getOutgoingEdgesFrom([node.id], ['extends', 'implements']).filter((e) => e.provenance !== 'heuristic')).toEqual([]); + } + }); + + it('answers a method only the project asks for with one the project declares', () => { + const into = (name: string, file: string) => + cg + .getIncomingEdgesTo([one(name, file).id], ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => pkg(cg.getNode(e.source))) + .sort(); + // waitRecorder's Lock and Unlock are sync.Mutex's. + expect(into('LockClient', 'fileutil/fileutil.go')).toEqual(['fileutil.lockProxy']); + // responseWriter's Header is http.ResponseWriter's, which ResponseWriter asks for. + expect(into('ResponseWriter', 'gin/response_writer.go')).toEqual(['gin.responseWriter']); + }); + + it('links an interface embedding an undescribed type to the same embedding, or to its one candidate', () => { + const into = (name: string) => + cg + .getIncomingEdgesTo([one(name, 'diag/diag.go').id], ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => pkg(cg.getNode(e.source))) + .sort(); + // Two structs have a Log: neither is known to be a plog.Logger. + expect(into('Diagnostic')).toEqual([]); + expect(into('LogBufferCoreInterface')).toEqual(['diag.LogBufferCore']); + expect(into('ClientStream')).toEqual(['diag.wrappedStream']); + expect(into('DNSProvider')).toEqual([]); + }); +}); + +describe('a sync that changes what an interface embeds', () => { + let dir = ''; + let graph: CodeGraph | undefined; + + afterEach(() => { + graph?.destroy(); + graph = undefined; + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = ''; + }); + + it('re-reads the standard-library type it names', async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-implements-stdlib-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 api = 'package api\n\nimport stdio "io"\n\ntype SyncWriter interface {\n\tstdio.Writer\n\tSync() error\n}\n'; + write('go.mod', 'module example.com/app\n\ngo 1.22\n'); + write('api/api.go', api); + write( + 'log/file.go', + 'package log\n\ntype file struct{}\n\nfunc (f *file) Write(p []byte) (int, error) { return len(p), nil }\nfunc (f *file) Sync() error { return nil }\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}`); + + expect(linked()).toEqual(['file -> SyncWriter']); + // An io.Reader has to Read, which file can't. + write('api/api.go', api.replace('stdio.Writer', 'stdio.Reader')); + await g.sync({ paths: ['api/api.go'] }); + expect(linked()).toEqual([]); + write('api/api.go', api); + await g.sync({ paths: ['api/api.go'] }); + expect(linked()).toEqual(['file -> SyncWriter']); + }, 60_000); +}); diff --git a/__tests__/go-interface-embedding.test.ts b/__tests__/go-interface-embedding.test.ts index 2883c6f50b..f1d1736c3d 100644 --- a/__tests__/go-interface-embedding.test.ts +++ b/__tests__/go-interface-embedding.test.ts @@ -18,9 +18,10 @@ * resolution reads the package back from the source. `storage.LabelQuerier` * reaches that package's interface; `io.Closer` and `sync.Mutex` stay * unresolved rather than landing on a project type that shares the name. - * Go's predeclared types are never recorded: `int64` is a constraint's term, - * and `error`, `any` and `comparable` exist in no graph. Runs against the - * native kernel (when built) and the wasm extractor, which must agree. + * Go's predeclared types are not recorded: `int64` is a constraint's term, + * and `any` and `comparable` have no methods. `error` has one, so embedding it + * is recorded, and stays unresolved like `io.Closer`. Runs against the native + * kernel (when built) and the wasm extractor, which must agree. * * Resolution keeps an embedded type in its package. A bare name is its own * package's type, not another package's struct of that name, which the Go @@ -204,6 +205,7 @@ describe('Go embedded types are supertypes', () => { 'Querier extends Closer', 'Querier extends LabelQuerier', 'Querier extends Queryable', + 'Timeout extends error', ]); expectRefsOnTheirNames(result, source); // The interfaces' own methods are still its members, the embeddings are not. @@ -228,6 +230,7 @@ describe('Go embedded types are supertypes', () => { 'Store extends List', 'Store extends Mutex', 'Store extends Reader', + 'Store extends error', ]); expectRefsOnTheirNames(result, source); }); diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index f0ccbab764..7135119803 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -859,7 +859,7 @@ impl<'t> Walker<'t> { /// embedding names — `T`, `*T` (the `*` is a sibling token), `pkg.T` and /// `T[X]` all embed `T`. A qualified type yields its name; resolution reads /// the package back from the source at that position. None for any other - /// type and for a predeclared one. + /// type and for a predeclared one but `error`, an interface with a method. fn embedded_type_name(&self, ty: Option>) -> Option> { let mut ty = ty?; if ty.kind() == "generic_type" { @@ -867,7 +867,10 @@ impl<'t> Walker<'t> { } match ty.kind() { "qualified_type" => ty.child_by_field_name("name"), - "type_identifier" if !is_go_predeclared_type(self.text(ty)) => Some(ty), + "type_identifier" => { + let name = self.text(ty); + (name == "error" || !is_go_predeclared_type(name)).then_some(ty) + } _ => None, } } diff --git a/src/db/queries.ts b/src/db/queries.ts index a063beff2b..03cf1df976 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -2986,6 +2986,42 @@ export class QueryBuilder { })); } + /** + * {@link getUnresolvedReferencesFrom} for many nodes at once, limited to + * `kinds`: a Go type's embeddings of types outside the index, say. Chunked + * probe over `idx_unresolved_from_node`. + */ + getUnresolvedReferencesFromNodes(nodeIds: Iterable, kinds: readonly string[]): UnresolvedReference[] { + const unique = [...new Set(nodeIds)]; + const found: UnresolvedReference[] = []; + if (unique.length === 0 || kinds.length === 0) return found; + const kindPlaceholders = kinds.map(() => '?').join(','); + for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) { + const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); + const rows = this.db + .prepare( + `SELECT * FROM unresolved_refs + WHERE from_node_id IN (${placeholders}) + AND reference_kind IN (${kindPlaceholders})` + ) + .all(...chunk, ...kinds) as UnresolvedRefRow[]; + for (const row of rows) { + found.push({ + fromNodeId: row.from_node_id, + referenceName: row.reference_name, + referenceKind: row.reference_kind as EdgeKind, + line: row.line, + column: row.col, + filePath: row.file_path, + language: row.language as Language, + rowId: row.id, + }); + } + } + return found; + } + /** * Find all edges where both source and target are in the given node set. * Useful for recovering inter-node connectivity after BFS. diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts index 1ecbf8612d..04ff6dd215 100644 --- a/src/extraction/languages/go.ts +++ b/src/extraction/languages/go.ts @@ -55,13 +55,15 @@ const GO_PREDECLARED_TYPES: ReadonlySet = new Set([ * embed `Base`. A qualified type yields its name, not its package: the package * stays in the source text, and resolution reads it back from the reference's * position (#2322). Undefined for any other type (a literal, `~T`, a union's - * term) and for a predeclared one. + * term) and for a predeclared one but `error`: an interface with a method, + * which an embedding brings in like any type's from outside the index. */ export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source: string): SyntaxNode | undefined { if (type?.type === 'generic_type') type = getChildByField(type, 'type'); if (type?.type === 'qualified_type') return getChildByField(type, 'name') ?? undefined; if (type?.type !== 'type_identifier') return undefined; - return GO_PREDECLARED_TYPES.has(getNodeText(type, source)) ? undefined : type; + const name = getNodeText(type, source); + return name === 'error' || !GO_PREDECLARED_TYPES.has(name) ? type : undefined; } /** diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index bc0fcc8bbe..6eea57274e 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -39,7 +39,8 @@ import { createYielder, type MaybeYield } from './cooperative-yield'; import { crossTierEdges, hasCrossTierPattern, hasTestRequestPattern, testRequestEdges } from './tier-synthesizer'; import { enclosingFn, makeLineAt } from './synth-utils'; import { resolveImportPath } from './import-resolver'; -import { crossesCodeBoundary, jsCodeBindsName } from './name-matcher'; +import { crossesCodeBoundary, goRefQualification, jsCodeBindsName } from './name-matcher'; +import { goStdlibMethods } from './go-stdlib-methods'; const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/; const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i; @@ -802,6 +803,15 @@ const IFACE_OVERRIDE_LANGS = new Set([ 'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust', 'arkts', ]); +/** A Go type's method names, by where they are known from. */ +interface GoMethodNames { + /** Declared in the project: the type's own, and those of the project types it embeds. */ + readonly declared: Set; + /** Those of the standard-library types it embeds, and `error`'s (see ./go-stdlib-methods). */ + readonly stdlib: ReadonlySet; + /** Embedded types from outside the index whose methods nothing names, by package and name. */ + readonly opaque: ReadonlySet; +} /** * Whether a supertype edge out of a Go type is one of its embeddings (#2397): * declared, never synthesized (go-implements writes those), into a struct, an @@ -839,8 +849,18 @@ function isGoEmbedding(e: Edge, kindOf: (id: string) => NodeKind | null): boolea * 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. + * never followed (this pass writes them). + * + * A type embedded from outside the index leaves only the resolver's failed + * `extends` row. The methods of a standard-library type, and of `error`, are + * known (./go-stdlib-methods): prometheus's `TBRun` is all of `testing.TB` + * plus `Run`, not any struct with a `Run`. Such a name answers only a method + * the standard library asks for too: etcd's gRPC `LockClient.Lock` is not the + * `Lock` an embedded `sync.Mutex` promotes. Any other outside type's methods + * are unknown, so a struct embedding the same type has them, and otherwise + * only the one struct with every method the interface does name is taken to + * implement it — kapacitor's `Diagnostic` (a third-party `Logger` plus `Log`) + * has no more claim on every struct with a `Log` than on none of them. * * Name-only matching (signatures ignored, and a promoted name counted even * where Go finds it ambiguous or hidden by a field) — over-approximation @@ -849,7 +869,7 @@ function isGoEmbedding(e: Edge, kindOf: (id: string) => NodeKind | null): boolea * type, and a struct that embeds the interface keeps the edge its embedding * already is. */ -async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise { +async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise { let scanned255 = 0; const edges: Edge[] = []; const seen = new Set(); @@ -912,48 +932,98 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr } const embeds = (id: string): string[] => embedMemo.get(id) ?? NO_EMBEDS; + // The types each one embeds from outside the index, as the resolver's failed + // `extends` rows: the methods of a standard-library type or of `error`, or + // else the embedded type itself, named by the package it is written through + // so that the same embedding elsewhere is recognized. + const outside = new Map[]; opaque: string[] }>(); + for (const ref of queries.getUnresolvedReferencesFromNodes(typeIds, ['extends'])) { + if (ref.language !== 'go' || !ref.filePath) continue; + const name = ref.referenceName; + const { written, imported } = goRefQualification({ ...ref, filePath: ref.filePath, language: 'go' }, ctx); + const known = imported ? goStdlibMethods(imported.source, name) : written ? undefined : goStdlibMethods(null, name); + let found = outside.get(ref.fromNodeId); + if (!found) outside.set(ref.fromNodeId, (found = { stdlib: [], opaque: [] })); + if (known) found.stdlib.push(known); + else if (imported) found.opaque.push(`${imported.source}.${name}`); + else if (written) found.opaque.push(`${written}.${name}`); + else found.opaque.push(`${ref.filePath.replace(/\\/g, '/').replace(/\/?[^/]*$/, '')}#${name}`); + } + await onYield(); + // 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 => { - if (embeds(id).length === 0) return ownMethods(id); - const names = new Set(); + const NONE_KNOWN: ReadonlySet = new Set(); + const methodNames = (id: string): GoMethodNames => { + if (embeds(id).length === 0 && !outside.has(id)) return { declared: ownMethods(id), stdlib: NONE_KNOWN, opaque: NONE_KNOWN }; + const declared = new Set(); + const stdlib = new Set(); + const opaque = new Set(); const reached = new Set([id]); const pending = [id]; while (pending.length > 0) { const at = pending.pop()!; - for (const m of ownMethods(at)) names.add(m); + for (const m of ownMethods(at)) declared.add(m); + const out = outside.get(at); + if (out) { + for (const names of out.stdlib) for (const m of names) stdlib.add(m); + for (const type of out.opaque) opaque.add(type); + } for (const t of embeds(at)) { if (reached.has(t)) continue; reached.add(t); pending.push(t); } } - return names; + return { declared, stdlib, opaque }; }; - const covers = (have: Set, want: Set): boolean => { - if (have.size < want.size) return false; - for (const m of want) { - if (!have.has(m)) return false; + // Every method `want` names, wherever it is known from: a name the standard + // library asks for is answered by any method of that name, and one only the + // project asks for by a method the project declares. + const coversNamed = (have: GoMethodNames, want: GoMethodNames): boolean => { + for (const m of want.declared) { + if (!have.declared.has(m) && !want.stdlib.has(m)) return false; + } + for (const m of want.stdlib) { + if (!have.declared.has(m) && !have.stdlib.has(m)) return false; + } + return true; + }; + const coversOpaque = (have: GoMethodNames, want: GoMethodNames): boolean => { + for (const type of want.opaque) { + if (!have.opaque.has(type)) return false; + } + return true; + }; + const declaresAll = (own: Set, want: GoMethodNames): boolean => { + if (want.opaque.size > 0) return false; + for (const m of want.declared) { + if (!own.has(m)) return false; + } + for (const m of want.stdlib) { + if (!own.has(m)) return false; } return true; }; - // A type without a method satisfies no interface this pass looks at. + // A type without a method, or an embedded type nothing describes, satisfies + // no interface this pass looks at. const candidates: Node[] = []; - const candidateMethods = new Map>(); + const candidateMethods = new Map(); for (const s of goImplementers) { if ((++scanned255 & 63) === 0) await onYield(); - const have = methodSet(s.id); - if (have.size === 0) continue; + const have = methodNames(s.id); + if (have.declared.size + have.stdlib.size + have.opaque.size === 0) continue; candidates.push(s); candidateMethods.set(s.id, have); } 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 + const want = methodNames(iface.id); + const named = want.stdlib.size === 0 ? want.declared.size : new Set([...want.declared, ...want.stdlib]).size; + if (named === 0 && want.opaque.size === 0) continue; // empty interface (`any`) — would match everything let added = 0; const link = (s: Node): void => { const key = `${s.id}>${iface.id}`; @@ -972,16 +1042,25 @@ 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. + // One that lacks only the embedded types nothing names the methods of + // counts when no other type has the rest. const throughEmbedding: Node[] = []; + let covering = 0; + let sole: Node | undefined; for (const s of candidates) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; const have = candidateMethods.get(s.id)!; - if (!covers(have, want)) continue; + if (have.declared.size + have.stdlib.size < named || !coversNamed(have, want)) continue; + covering++; 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); + if (!coversOpaque(have, want)) { + if (covering === 1) sole = s; + continue; + } + if (declaresAll(ownMethods(s.id), want)) link(s); + else throughEmbedding.push(s); } + if (sole && covering === 1 && named > 0) throughEmbedding.push(sole); for (const s of throughEmbedding) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; link(s); @@ -4389,7 +4468,7 @@ export async function synthesizeCallbackEdges( // interface-dispatch bridge below reads `implements` edges from the DB, and // Go has none statically. (Other languages already have static implements // edges from extraction, so they don't need this pre-pass.) - const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE; + const goImpl = has('go') ? await goImplementsEdges(queries, ctx, yieldToLoop) : NONE; for (let i = 0; i < goImpl.length; i += 2000) { queries.insertEdges(goImpl.slice(i, i + 2000)); await yieldToLoop(); diff --git a/src/resolution/go-stdlib-methods.ts b/src/resolution/go-stdlib-methods.ts new file mode 100644 index 0000000000..efc4b6273f --- /dev/null +++ b/src/resolution/go-stdlib-methods.ts @@ -0,0 +1,246 @@ +/** + * The method names of the standard-library types Go code embeds, for Go's + * implicit interface satisfaction (#584). An embedded type from outside the + * index (`io.Closer`, `http.ResponseWriter`, `*os.File`) resolves to nothing, + * so without this an interface embedding `testing.TB` asked only for its own + * `Run`, and a struct embedding `*os.File` had no `Read`. + * + * Keyed by import path, then type name; each value lists the type's method + * names, space-separated. An interface lists every method, embedded ones + * included, and so does a struct with those its embedded fields promote. + * Unexported names are kept where an interface requires one: `testing.TB`'s + * `private` and `reflect.Type`'s `common` and `uncommon` are why only an + * embedding satisfies those outside their own package. + * + * Read from Go's own source (development tip, October 2026). The interfaces + * are the ones projects embed; the structs are the commonly embedded ones + * that implement one of them. A struct whose methods would answer one of + * these interfaces' names with another signature (`http.Header.Write`, + * `sync.WaitGroup.Done`) is left out. + */ +const GO_STDLIB_METHODS: Readonly>>> = { + io: { + Reader: 'Read', + Writer: 'Write', + Closer: 'Close', + Seeker: 'Seek', + ReadWriter: 'Read Write', + ReadCloser: 'Close Read', + WriteCloser: 'Close Write', + ReadWriteCloser: 'Close Read Write', + ReadSeeker: 'Read Seek', + ReadSeekCloser: 'Close Read Seek', + WriteSeeker: 'Seek Write', + ReadWriteSeeker: 'Read Seek Write', + ReaderFrom: 'ReadFrom', + WriterTo: 'WriteTo', + ReaderAt: 'ReadAt', + WriterAt: 'WriteAt', + ByteReader: 'ReadByte', + ByteScanner: 'ReadByte UnreadByte', + ByteWriter: 'WriteByte', + RuneReader: 'ReadRune', + RuneScanner: 'ReadRune UnreadRune', + StringWriter: 'WriteString', + PipeReader: 'Close CloseWithError Read', + PipeWriter: 'Close CloseWithError Write', + SectionReader: 'Outer Read ReadAt Seek Size', + }, + 'io/fs': { + FS: 'Open', + File: 'Close Read Stat', + DirEntry: 'Info IsDir Name Type', + FileInfo: 'IsDir ModTime Mode Name Size Sys', + ReadDirFile: 'Close Read ReadDir Stat', + ReadDirFS: 'Open ReadDir', + ReadFileFS: 'Open ReadFile', + StatFS: 'Open Stat', + SubFS: 'Open Sub', + GlobFS: 'Glob Open', + }, + os: { + File: 'Chdir Chmod Chown Close Fd Name PollFD Read ReadAt ReadDir ReadFrom Readdir Readdirnames Seek SetDeadline SetReadDeadline SetWriteDeadline Stat Sync SyscallConn Truncate Write WriteAt WriteString WriteTo', + FileInfo: 'IsDir ModTime Mode Name Size Sys', + DirEntry: 'Info IsDir Name Type', + Signal: 'Signal String', + }, + fmt: { + Stringer: 'String', + GoStringer: 'GoString', + Formatter: 'Format', + State: 'Flag Precision Width Write', + Scanner: 'Scan', + ScanState: 'Read ReadRune SkipSpace Token UnreadRune Width', + }, + sort: { + Interface: 'Len Less Swap', + }, + 'container/heap': { + Interface: 'Len Less Pop Push Swap', + }, + context: { + Context: 'Deadline Done Err Value', + }, + sync: { + Locker: 'Lock Unlock', + Mutex: 'Lock TryLock Unlock', + RWMutex: 'Lock RLock RLocker RUnlock TryLock TryRLock Unlock', + }, + net: { + Conn: 'Close LocalAddr Read RemoteAddr SetDeadline SetReadDeadline SetWriteDeadline Write', + PacketConn: 'Close LocalAddr ReadFrom SetDeadline SetReadDeadline SetWriteDeadline WriteTo', + Listener: 'Accept Addr Close', + Addr: 'Network String', + Error: 'Error Temporary Timeout', + TCPConn: 'Close CloseRead CloseWrite File LocalAddr MultipathTCP Read ReadFrom RemoteAddr SetDeadline SetKeepAlive SetKeepAliveConfig SetKeepAlivePeriod SetLinger SetNoDelay SetReadBuffer SetReadDeadline SetWriteBuffer SetWriteDeadline SyscallConn Write WriteTo', + UDPConn: 'Close File LocalAddr Read ReadFrom ReadFromUDP ReadFromUDPAddrPort ReadMsgUDP ReadMsgUDPAddrPort RemoteAddr SetDeadline SetReadBuffer SetReadDeadline SetWriteBuffer SetWriteDeadline SyscallConn Write WriteMsgUDP WriteMsgUDPAddrPort WriteTo WriteToUDP WriteToUDPAddrPort', + UnixConn: 'Close CloseRead CloseWrite File LocalAddr Read ReadFrom ReadFromUnix ReadMsgUnix RemoteAddr SetDeadline SetReadBuffer SetReadDeadline SetWriteBuffer SetWriteDeadline SyscallConn Write WriteMsgUnix WriteTo WriteToUnix', + TCPListener: 'Accept AcceptTCP Addr Close File SetDeadline SyscallConn', + UnixListener: 'Accept AcceptUnix Addr Close File SetDeadline SetUnlinkOnClose SyscallConn', + }, + 'net/http': { + ResponseWriter: 'Header Write WriteHeader', + Hijacker: 'Hijack', + Flusher: 'Flush', + CloseNotifier: 'CloseNotify', + Pusher: 'Push', + Handler: 'ServeHTTP', + RoundTripper: 'RoundTrip', + File: 'Close Read Readdir Seek Stat', + FileSystem: 'Open', + CookieJar: 'Cookies SetCookies', + ServeMux: 'Handle HandleFunc Handler ServeHTTP', + Transport: 'CancelRequest Clone CloseIdleConnections NewClientConn RegisterProtocol RoundTrip', + }, + 'net/http/httptest': { + ResponseRecorder: 'Flush Header Result Write WriteHeader WriteString', + }, + bufio: { + Reader: 'Buffered Discard Peek Read ReadByte ReadBytes ReadLine ReadRune ReadSlice ReadString Reset Size UnreadByte UnreadRune WriteTo', + Writer: 'Available AvailableBuffer Buffered Flush ReadFrom Reset Size Write WriteByte WriteRune WriteString', + ReadWriter: 'Available AvailableBuffer Buffered Discard Flush Peek Read ReadByte ReadBytes ReadFrom ReadLine ReadRune ReadSlice ReadString Reset Size UnreadByte UnreadRune Write WriteByte WriteRune WriteString WriteTo', + }, + bytes: { + Buffer: 'Available AvailableBuffer Bytes Cap Grow Len Next Peek Read ReadByte ReadBytes ReadFrom ReadRune ReadString Reset String Truncate UnreadByte UnreadRune Write WriteByte WriteRune WriteString WriteTo', + Reader: 'Len Read ReadAt ReadByte ReadRune Reset Seek Size UnreadByte UnreadRune WriteTo', + }, + strings: { + Reader: 'Len Read ReadAt ReadByte ReadRune Reset Seek Size UnreadByte UnreadRune WriteTo', + Builder: 'Cap Grow Len Reset String Write WriteByte WriteRune WriteString', + }, + testing: { + TB: 'ArtifactDir Attr Chdir Cleanup Context Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Name Output Setenv Skip SkipNow Skipf Skipped TempDir private', + T: 'ArtifactDir Attr Chdir Cleanup Context Deadline Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Name Output Parallel Run Setenv Skip SkipNow Skipf Skipped TempDir private', + B: 'ArtifactDir Attr Chdir Cleanup Context Elapsed Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Loop Name Output ReportAllocs ReportMetric ResetTimer Run RunParallel SetBytes SetParallelism Setenv Skip SkipNow Skipf Skipped StartTimer StopTimer TempDir private', + F: 'Add ArtifactDir Attr Chdir Cleanup Context Error Errorf Fail FailNow Failed Fatal Fatalf Fuzz Helper Log Logf Name Output Setenv Skip SkipNow Skipf Skipped TempDir private', + }, + 'log/slog': { + Handler: 'Enabled Handle WithAttrs WithGroup', + JSONHandler: 'Enabled Handle WithAttrs WithGroup', + TextHandler: 'Enabled Handle WithAttrs WithGroup', + }, + encoding: { + BinaryMarshaler: 'MarshalBinary', + BinaryUnmarshaler: 'UnmarshalBinary', + TextMarshaler: 'MarshalText', + TextUnmarshaler: 'UnmarshalText', + }, + 'encoding/json': { + Marshaler: 'MarshalJSON', + Unmarshaler: 'UnmarshalJSON', + RawMessage: 'MarshalJSON UnmarshalJSON', + }, + 'encoding/xml': { + Marshaler: 'MarshalXML', + Unmarshaler: 'UnmarshalXML', + }, + 'encoding/gob': { + GobEncoder: 'GobEncode', + GobDecoder: 'GobDecode', + }, + expvar: { + Var: 'String', + }, + hash: { + Hash: 'BlockSize Reset Size Sum Write', + Hash32: 'BlockSize Reset Size Sum Sum32 Write', + Hash64: 'BlockSize Reset Size Sum Sum64 Write', + }, + crypto: { + Signer: 'Public Sign', + Decrypter: 'Decrypt Public', + }, + 'crypto/elliptic': { + Curve: 'Add Double IsOnCurve Params ScalarBaseMult ScalarMult', + }, + 'crypto/cipher': { + Block: 'BlockSize Decrypt Encrypt', + Stream: 'XORKeyStream', + AEAD: 'NonceSize Open Overhead Seal', + BlockMode: 'BlockSize CryptBlocks', + }, + 'crypto/tls': { + Conn: 'Close CloseWrite ConnectionState Handshake HandshakeContext LocalAddr NetConn OCSPResponse Read RemoteAddr SetDeadline SetReadDeadline SetWriteDeadline VerifyHostname Write', + }, + image: { + Image: 'At Bounds ColorModel', + PalettedImage: 'At Bounds ColorIndexAt ColorModel', + }, + 'image/color': { + Color: 'RGBA', + Model: 'Convert', + }, + 'database/sql': { + Scanner: 'Scan', + }, + 'database/sql/driver': { + Valuer: 'Value', + Driver: 'Open', + Conn: 'Begin Close Prepare', + Stmt: 'Close Exec NumInput Query', + Tx: 'Commit Rollback', + Rows: 'Close Columns Next', + Result: 'LastInsertId RowsAffected', + }, + flag: { + Value: 'Set String', + Getter: 'Get Set String', + }, + runtime: { + Error: 'Error RuntimeError', + }, + reflect: { + Type: 'Align AssignableTo Bits CanSeq CanSeq2 ChanDir Comparable ConvertibleTo Elem Field FieldAlign FieldByIndex FieldByName FieldByNameFunc Fields Implements In Ins IsVariadic Key Kind Len Method MethodByName Methods Name NumField NumIn NumMethod NumOut Out Outs OverflowComplex OverflowFloat OverflowInt OverflowUint PkgPath Size String common uncommon', + }, + time: { + Time: 'Add AddDate After AppendBinary AppendFormat AppendText Before Clock Compare Date Day Equal Format GoString GobDecode GobEncode Hour ISOWeek In IsDST IsZero Local Location MarshalBinary MarshalJSON MarshalText Minute Month Nanosecond Round Second String Sub Truncate UTC Unix UnixMicro UnixMilli UnixNano UnmarshalBinary UnmarshalJSON UnmarshalText Weekday Year YearDay Zone ZoneBounds', + }, + 'compress/gzip': { + Reader: 'Close Multistream Read Reset', + Writer: 'Close Flush Reset Write', + }, +}; + +/** `error`, the one predeclared type with a method. */ +const GO_ERROR_METHODS: ReadonlySet = new Set(['Error']); + +const parsed = new Map | null>(); + +/** + * The method names of the standard-library type `typeName` that the package + * at `importPath` declares, or of the predeclared `error` when `importPath` + * is null. Undefined for a type this table does not know. + */ +export function goStdlibMethods(importPath: string | null, typeName: string): ReadonlySet | undefined { + if (importPath === null) return typeName === 'error' ? GO_ERROR_METHODS : undefined; + const key = `${importPath}.${typeName}`; + let methods = parsed.get(key); + if (methods === undefined) { + const names = Object.prototype.hasOwnProperty.call(GO_STDLIB_METHODS, importPath) + ? GO_STDLIB_METHODS[importPath]![typeName] + : undefined; + methods = typeof names === 'string' ? new Set(names.split(' ')) : null; + parsed.set(key, methods); + } + return methods ?? undefined; +} diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 46dd84752d..35e9097682 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1918,7 +1918,7 @@ function rustModuleDir(filePath: string): string { return path.posix.join(dir, base.replace(/\.rs$/, '')); } -interface GoQualification { +export interface GoQualification { /** The package name written before the reference's name, as spelled. */ written?: string; /** The file's import that name is. */ @@ -1927,7 +1927,12 @@ interface GoQualification { const GO_QUALIFIERS = new WeakMap>(); -function goRefQualification(ref: UnresolvedRef, context: ResolutionContext): GoQualification { +/** + * The package a Go reference is written through, read from its line (the + * index keeps only the name): `io` in `io.Closer`, and the file's import of + * it. Empty for a name written bare. + */ +export function goRefQualification(ref: UnresolvedRef, context: ResolutionContext): GoQualification { if (ref.referenceKind === 'imports') return {}; const name = ref.referenceName.split('.').pop()!; if (!/^[A-Za-z_]\w*$/.test(name)) return {}; From 30ed50ede27870b81cdec6b22a4a228fd0737bcc Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 10:43:58 -0500 Subject: [PATCH 2/6] docs(changelog): Go counts the methods of embedded standard-library types Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fde26e979e..8c13422854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - 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, the methods a type gets from an embedded standard-library type, like `io.Closer`, `http.ResponseWriter`, `testing.TB` or `*os.File`, or from the built-in `error`, now count when working out which types implement an interface. Before, they were left out: prometheus's `TBRun`, which is all of `testing.TB` plus `Run`, was listed as implemented by its service discoveries because each has a `Run`, and an interface made only of standard-library ones, like `slog.Handler` plus `io.Closer`, had no implementations at all. An interface that embeds a type from another module, whose methods can't be seen, is now implemented only by a type that embeds the same one or by the only type with all its other methods, not by every type that shares a method name with it. 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 Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading. From fb8ad0d454b781ff50adecc71dff314fb9ba9038 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 21 Oct 2026 12:28:24 -0500 Subject: [PATCH 3/6] fix(go): standard-library methods carry their counts and their package's key Re-applies the standard-library embedding knowledge to goImplementsEdges as main has it now (#2447's arity gate, #2746's goMethodKey): - The table lists each method as `name:params/results`, regenerated from go1.27.3's source with the counting goSignatureArity applies to a stored signature, so the arity gate holds a project method answering an `io.Writer`'s `Write` to one parameter and two results. A method an embedding brings from the table joins the type's arities too. - A standard-library method is keyed like a project's: an exported name alone, an unexported one (`testing.TB`'s `private`) with its package, spelled `std:` so it is no directory of the project's. - On a gRPC client interface's path, a method only a standard-library embedding asks for keeps the table's arity. Co-Authored-By: Claude Opus 5.5 --- __tests__/go-implements-stdlib.test.ts | 42 +++- src/resolution/callback-synthesizer.ts | 169 ++++++++++--- src/resolution/go-stdlib-methods.ts | 313 +++++++++++++------------ 3 files changed, 331 insertions(+), 193 deletions(-) diff --git a/__tests__/go-implements-stdlib.test.ts b/__tests__/go-implements-stdlib.test.ts index 43b6e49f75..b7ab75242b 100644 --- a/__tests__/go-implements-stdlib.test.ts +++ b/__tests__/go-implements-stdlib.test.ts @@ -11,12 +11,14 @@ * asked only for `Run`, and 40 service discoverers "implemented" it. The * embedded `error` was not even recorded. * - * The method names of the standard-library types projects embed are known - * now, and `error` is recorded as the embedding it is. Such a name answers - * only what the standard library asks for: an embedded `sync.Mutex` gives no - * struct etcd's gRPC `LockClient.Lock`. Embedding any other outside type - * (`zapcore.Core`, `grpc.ClientStream`) asks for methods nothing names: a - * struct embedding the same type has them, and otherwise only the one struct + * The methods of the standard-library types projects embed are known now, + * with their parameter and result counts, and `error` is recorded as the + * embedding it is. Such a method answers only what the standard library asks + * for: an embedded `sync.Mutex` gives no struct etcd's gRPC `LockClient.Lock`. + * An unexported one is its own package's, as Go has it: `testing.TB`'s + * `private`, which no project type can declare. Embedding any other outside + * type (`zapcore.Core`, `grpc.ClientStream`) asks for methods nothing names: + * a struct embedding the same type has them, and otherwise only the one struct * with every method the interface does name is taken to implement it. */ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; @@ -25,8 +27,25 @@ import * as os from 'os'; import * as path from 'path'; import CodeGraph from '../src/index'; import { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import { goStdlibMethods } from '../src/resolution/go-stdlib-methods'; import type { Edge, Node } from '../src/types'; +/** + * A project struct declaring every method `testing.TB` has, at its counts, + * its unexported `private` included, and a TBRun's `Run`: in a package whose + * directory is itself named `testing`, its `private` is still not testing's. + */ +function fakeTestingTB(): string { + const lines = ['package testing', '', 'type fakeTB struct{}', '']; + for (const [name, arity] of [...goStdlibMethods('testing', 'TB')!, ['Run', '2/1'] as const]) { + const [params, results] = arity.split('/').map(Number) as [number, number]; + const list = Array.from({ length: params }, (_, i) => `a${i} int`).join(', '); + const result = results === 0 ? '' : results === 1 ? ' int' : ` (${Array(results).fill('int').join(', ')})`; + lines.push(`func (f *fakeTB) ${name}(${list})${result} { panic(0) }`); + } + return `${lines.join('\n')}\n`; +} + const FILES: Record = { 'go.mod': 'module example.com/app\n\ngo 1.22\n', // prometheus. @@ -71,6 +90,12 @@ type queryLogFile struct{} func (f *queryLogFile) Write(p []byte) (int, error) { return len(p), nil } func (f *queryLogFile) Sync() error { return nil } +// Its Write takes a record and returns only an error: no io.Writer's. +type walLog struct{} + +func (w *walLog) Write(rec *Record) error { return nil } +func (w *walLog) Sync() error { return nil } + type SegmentFile interface { Stat() (os.FileInfo, error) Sync() error @@ -282,6 +307,7 @@ type DNSProvider interface { libdns.RecordSetter } `, + 'testing/fake.go': fakeTestingTB(), }; let root = ''; @@ -338,10 +364,12 @@ describe('Go implicit satisfaction knows what standard-library embeddings bring' ]); }); - it('asks for all of testing.TB, so a discoverer with a Run is no TBRun', () => { + it('asks for every method of testing.TB and io.Writer, testing\'s own private and io\'s counts included', () => { const implementers = (name: string, file: string) => (buildTypeHierarchy(cg, one(name, file))?.descendants ?? []).map((d) => `${d.relation} ${pkg(d.node)}`).sort(); + // Not the discoverer with a Run, nor fakeTB, whose private is its own package's. expect(implementers('TBRun', 'promql/promql.go')).toEqual(['implements promql.tRunner']); + // Not walLog, whose Write takes a record and returns only an error. expect(implementers('syncWriter', 'promql/promql.go')).toEqual([ 'implements promql.Segment', 'implements promql.queryLogFile', diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index f54ce3c44f..e53d5b3748 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -40,7 +40,8 @@ import { createYielder, type MaybeYield } from './cooperative-yield'; import { crossTierEdges, hasCrossTierPattern, hasTestRequestPattern, testRequestEdges } from './tier-synthesizer'; import { enclosingFn, makeLineAt } from './synth-utils'; import { resolveImportPath } from './import-resolver'; -import { cppSpecializationArguments, crossesCodeBoundary, jsCodeBindsName } from './name-matcher'; +import { cppSpecializationArguments, crossesCodeBoundary, goRefQualification, jsCodeBindsName } from './name-matcher'; +import { goStdlibMethods } from './go-stdlib-methods'; import { declaredComponent, exportedComponent, lazyValueModules, wrappedComponentName } from './frameworks/react'; import { jsxCode } from './jsx-code'; @@ -843,6 +844,23 @@ function goMethodKey(m: Node): string { const file = m.filePath.replace(/\\/g, '/'); return `${file.slice(0, Math.max(file.lastIndexOf('/'), 0))}\0${m.name}`; } +/** + * {@link goMethodKey} for a method of a standard-library type: its name, and + * for an unexported one (`testing.TB`'s `private`) the declaring package, + * spelled so it is no directory of the project's. + */ +function goStdlibMethodKey(importPath: string | null, name: string): string { + return GO_EXPORTED_NAME.test(name) ? name : `std:${importPath ?? ''}\0${name}`; +} +/** A Go type's method keys ({@link goMethodKey}), by where each is known from. */ +interface GoMethodNames { + /** Declared in the project: the type's own, and those of the project types it embeds. */ + readonly declared: Set; + /** Those of the standard-library types it embeds, and `error`'s (see ./go-stdlib-methods). */ + readonly stdlib: ReadonlySet; + /** Embedded types from outside the index whose methods nothing names, by package and name. */ + readonly opaque: ReadonlySet; +} /** * 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` @@ -958,8 +976,18 @@ function goGrpcServerArity(signature: string | undefined): string | null { * 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. + * never followed (this pass writes them). + * + * A type embedded from outside the index leaves only the resolver's failed + * `extends` row. The methods of a standard-library type, and of `error`, are + * known (./go-stdlib-methods): prometheus's `TBRun` is all of `testing.TB` + * plus `Run`, not any struct with a `Run`. Such a method answers only one the + * standard library asks for too: etcd's gRPC `LockClient.Lock` is not the + * `Lock` an embedded `sync.Mutex` promotes. Any other outside type's methods + * are unknown, so a struct embedding the same type has them, and otherwise + * only the one type with every method the interface does name is taken to + * implement it — kapacitor's `Diagnostic` (a third-party `Logger` plus `Log`) + * has no more claim on every struct with a `Log` than on none of them. * * Methods match by name, and an unexported name by its package too * ({@link goMethodKey}). prometheus has a `refresher { refresh(…) }` in each of @@ -973,10 +1001,12 @@ function goGrpcServerArity(signature: string | undefined): string | null { * 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, - * key, opts ...OpOption)` is no `Get(id types.ID) Peer`. A signature that - * doesn't read rules nothing out. A gRPC client interface also takes the - * structs serving its service (see `wantedArities`), the bridge a client's - * call crosses to reach its handler. + * key, opts ...OpOption)` is no `Get(id types.ID) Peer`. A standard-library + * method carries its counts in the table, so an `io.Writer`'s `Write` asks for + * one parameter and two results. A signature that doesn't read rules nothing + * out. A gRPC client interface also takes the structs serving its service + * (see `wantedArities`), the bridge a client's call crosses to reach its + * handler. * * Types are not compared, and a promoted name counts even where Go finds it * ambiguous or hidden by a field — over-approximation accepted, in line with @@ -984,7 +1014,7 @@ function goGrpcServerArity(signature: string | undefined): string | null { * (`any`) are skipped so they don't match every type, and a struct that * embeds the interface keeps the edge its embedding already is. */ -async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise { +async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise { let scanned255 = 0; const edges: Edge[] = []; const seen = new Set(); @@ -1073,27 +1103,79 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr return [...reached]; }; - // Own methods plus every embedded type's. - const methodSet = (id: string): Set => { + // The types each one embeds from outside the index, as the resolver's failed + // `extends` rows: the methods of a standard-library type or of `error`, by + // key with their arities, or else the embedded type itself, named by the + // package it is written through so that the same embedding elsewhere is + // recognized. + const outside = new Map[]; opaque: string[] }>(); + for (const ref of queries.getUnresolvedReferencesFromNodes(typeIds, ['extends'])) { + if (ref.language !== 'go' || !ref.filePath) continue; + const name = ref.referenceName; + const { written, imported } = goRefQualification({ ...ref, filePath: ref.filePath, language: 'go' }, ctx); + const importPath = imported?.source ?? null; + const known = imported ? goStdlibMethods(importPath, name) : written ? undefined : goStdlibMethods(null, name); + let found = outside.get(ref.fromNodeId); + if (!found) outside.set(ref.fromNodeId, (found = { stdlib: [], opaque: [] })); + if (known) found.stdlib.push(new Map([...known].map(([m, arity]) => [goStdlibMethodKey(importPath, m), arity]))); + else if (imported) found.opaque.push(`${imported.source}.${name}`); + else if (written) found.opaque.push(`${written}.${name}`); + else found.opaque.push(`${ref.filePath.replace(/\\/g, '/').replace(/\/?[^/]*$/, '')}#${name}`); + } + await onYield(); + + // Own methods plus every embedded type's, by where each is known from. + const NONE_KNOWN: ReadonlySet = new Set(); + const methodNames = (id: string): GoMethodNames => { const types = embedClosure(id); - if (types.length === 1) return ownMethods(id); - const names = new Set(); - for (const t of types) for (const m of ownMethods(t)) names.add(m); - return names; + if (types.length === 1 && !outside.has(id)) return { declared: ownMethods(id), stdlib: NONE_KNOWN, opaque: NONE_KNOWN }; + const declared = new Set(); + const stdlib = new Set(); + const opaque = new Set(); + for (const t of types) { + for (const m of ownMethods(t)) declared.add(m); + const out = outside.get(t); + if (!out) continue; + for (const known of out.stdlib) for (const key of known.keys()) stdlib.add(key); + for (const type of out.opaque) opaque.add(type); + } + return { declared, stdlib, opaque }; }; - const covers = (have: Set, want: Set): boolean => { - if (have.size < want.size) return false; - for (const m of want) { - if (!have.has(m)) return false; + // Every method `want` names, wherever it is known from: a method the + // standard library asks for is answered by any method of that key, and one + // only the project asks for by a method the project declares. + const coversNamed = (have: GoMethodNames, want: GoMethodNames): boolean => { + for (const m of want.declared) { + if (!have.declared.has(m) && !want.stdlib.has(m)) return false; + } + for (const m of want.stdlib) { + if (!have.declared.has(m) && !have.stdlib.has(m)) return false; + } + return true; + }; + const coversOpaque = (have: GoMethodNames, want: GoMethodNames): boolean => { + for (const type of want.opaque) { + if (!have.opaque.has(type)) return false; + } + return true; + }; + const declaresAll = (own: Set, want: GoMethodNames): boolean => { + if (want.opaque.size > 0) return false; + for (const m of want.declared) { + if (!own.has(m)) return false; + } + for (const m of want.stdlib) { + if (!own.has(m)) return false; } return true; }; // 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. + // signatures doesn't read (then the name rules nothing out). A method a + // standard-library embedding brings has the table's. 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); @@ -1114,6 +1196,14 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr known.add(arity); } } + for (const methods of outside.get(t)?.stdlib ?? []) { + for (const [name, arity] of methods) { + let known = arities.get(name); + if (known === null) continue; + if (!known) arities.set(name, (known = new Set())); + known.add(arity); + } + } } arityMemo.set(id, arities); return arities; @@ -1137,6 +1227,8 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr } } } + // What only a standard-library embedding asks for keeps the table's arity. + for (const [name, arities] of own) if (!wanted.has(name)) wanted.set(name, arities); return wanted; }; @@ -1161,21 +1253,23 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr return true; }; - // A type without a method satisfies no interface this pass looks at. + // A type without a method, or an embedded type nothing describes, satisfies + // no interface this pass looks at. const candidates: Node[] = []; - const candidateMethods = new Map>(); + const candidateMethods = new Map(); for (const s of goImplementers) { if ((++scanned255 & 63) === 0) await onYield(); - const have = methodSet(s.id); - if (have.size === 0) continue; + const have = methodNames(s.id); + if (have.declared.size + have.stdlib.size + have.opaque.size === 0) continue; candidates.push(s); candidateMethods.set(s.id, have); } 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 + const want = methodNames(iface.id); + const named = want.stdlib.size === 0 ? want.declared.size : new Set([...want.declared, ...want.stdlib]).size; + if (named === 0 && want.opaque.size === 0) continue; // empty interface (`any`) — would match everything let added = 0; const link = (s: Node): void => { const key = `${s.id}>${iface.id}`; @@ -1195,20 +1289,29 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr // of them, every defined type among them: under the cap, those are what // 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. + // not one of them: the method it needs is promoted. One that lacks only + // the embedded types nothing names the methods of counts when no other + // type has the rest. const throughEmbedding: Node[] = []; let wanted: Map | null> | undefined; + let covering = 0; + let sole: Node | undefined; for (const s of candidates) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; const have = candidateMethods.get(s.id)!; - if (!covers(have, want)) continue; - if (embeds(s.id).includes(iface.id)) continue; // declared by embedding it + if (have.declared.size + have.stdlib.size < named || !coversNamed(have, want)) continue; if (!wanted) wanted = wantedArities(iface); if (!aritiesFit(s.id, wanted)) continue; - const own = ownMethods(s.id); - if (own !== have && !covers(own, want)) throughEmbedding.push(s); - else link(s); + covering++; + if (embeds(s.id).includes(iface.id)) continue; // declared by embedding it + if (!coversOpaque(have, want)) { + if (covering === 1) sole = s; + continue; + } + if (declaresAll(ownMethods(s.id), want)) link(s); + else throughEmbedding.push(s); } + if (sole && covering === 1 && named > 0) throughEmbedding.push(sole); for (const s of throughEmbedding) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; link(s); @@ -5181,7 +5284,7 @@ export async function synthesizeCallbackEdges( // interface-dispatch bridge below reads `implements` edges from the DB, and // Go has none statically. (Other languages already have static implements // edges from extraction, so they don't need this pre-pass.) - const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE; + const goImpl = has('go') ? await goImplementsEdges(queries, ctx, yieldToLoop) : NONE; for (let i = 0; i < goImpl.length; i += 2000) { queries.insertEdges(goImpl.slice(i, i + 2000)); await yieldToLoop(); diff --git a/src/resolution/go-stdlib-methods.ts b/src/resolution/go-stdlib-methods.ts index efc4b6273f..a2c85387ad 100644 --- a/src/resolution/go-stdlib-methods.ts +++ b/src/resolution/go-stdlib-methods.ts @@ -1,245 +1,252 @@ /** - * The method names of the standard-library types Go code embeds, for Go's - * implicit interface satisfaction (#584). An embedded type from outside the - * index (`io.Closer`, `http.ResponseWriter`, `*os.File`) resolves to nothing, - * so without this an interface embedding `testing.TB` asked only for its own + * The methods of the standard-library types Go code embeds, for Go's implicit + * interface satisfaction (#584). An embedded type from outside the index + * (`io.Closer`, `http.ResponseWriter`, `*os.File`) resolves to nothing, so + * without this an interface embedding `testing.TB` asked only for its own * `Run`, and a struct embedding `*os.File` had no `Read`. * - * Keyed by import path, then type name; each value lists the type's method - * names, space-separated. An interface lists every method, embedded ones - * included, and so does a struct with those its embedded fields promote. - * Unexported names are kept where an interface requires one: `testing.TB`'s - * `private` and `reflect.Type`'s `common` and `uncommon` are why only an - * embedding satisfies those outside their own package. + * Keyed by import path, then type name; each value lists the type's methods, + * space-separated, as `name:params/results`: the parameter and result counts + * goSignatureArity reads off a stored signature, so the arity gate holds a + * project method answering one to the same counts. An interface lists every + * method, embedded ones included, and so does a struct with those its embedded + * fields promote. Unexported names are kept where an interface requires one: + * `testing.TB`'s `private` and `reflect.Type`'s `common` and `uncommon` are + * why only an embedding satisfies those outside their own package. * - * Read from Go's own source (development tip, October 2026). The interfaces - * are the ones projects embed; the structs are the commonly embedded ones - * that implement one of them. A struct whose methods would answer one of - * these interfaces' names with another signature (`http.Header.Write`, - * `sync.WaitGroup.Done`) is left out. + * Read from Go's own source (go1.27.3). The interfaces are the ones projects + * embed; the structs are the commonly embedded ones that implement one of + * them. A struct whose methods would answer one of these interfaces' names + * with another signature (`http.Header.Write`, `sync.WaitGroup.Done`) is left + * out. */ const GO_STDLIB_METHODS: Readonly>>> = { io: { - Reader: 'Read', - Writer: 'Write', - Closer: 'Close', - Seeker: 'Seek', - ReadWriter: 'Read Write', - ReadCloser: 'Close Read', - WriteCloser: 'Close Write', - ReadWriteCloser: 'Close Read Write', - ReadSeeker: 'Read Seek', - ReadSeekCloser: 'Close Read Seek', - WriteSeeker: 'Seek Write', - ReadWriteSeeker: 'Read Seek Write', - ReaderFrom: 'ReadFrom', - WriterTo: 'WriteTo', - ReaderAt: 'ReadAt', - WriterAt: 'WriteAt', - ByteReader: 'ReadByte', - ByteScanner: 'ReadByte UnreadByte', - ByteWriter: 'WriteByte', - RuneReader: 'ReadRune', - RuneScanner: 'ReadRune UnreadRune', - StringWriter: 'WriteString', - PipeReader: 'Close CloseWithError Read', - PipeWriter: 'Close CloseWithError Write', - SectionReader: 'Outer Read ReadAt Seek Size', + Reader: 'Read:1/2', + Writer: 'Write:1/2', + Closer: 'Close:0/1', + Seeker: 'Seek:2/2', + ReadWriter: 'Read:1/2 Write:1/2', + ReadCloser: 'Close:0/1 Read:1/2', + WriteCloser: 'Close:0/1 Write:1/2', + ReadWriteCloser: 'Close:0/1 Read:1/2 Write:1/2', + ReadSeeker: 'Read:1/2 Seek:2/2', + ReadSeekCloser: 'Close:0/1 Read:1/2 Seek:2/2', + WriteSeeker: 'Seek:2/2 Write:1/2', + ReadWriteSeeker: 'Read:1/2 Seek:2/2 Write:1/2', + ReaderFrom: 'ReadFrom:1/2', + WriterTo: 'WriteTo:1/2', + ReaderAt: 'ReadAt:2/2', + WriterAt: 'WriteAt:2/2', + ByteReader: 'ReadByte:0/2', + ByteScanner: 'ReadByte:0/2 UnreadByte:0/1', + ByteWriter: 'WriteByte:1/1', + RuneReader: 'ReadRune:0/3', + RuneScanner: 'ReadRune:0/3 UnreadRune:0/1', + StringWriter: 'WriteString:1/2', + PipeReader: 'Close:0/1 CloseWithError:1/1 Read:1/2', + PipeWriter: 'Close:0/1 CloseWithError:1/1 Write:1/2', + SectionReader: 'Outer:0/3 Read:1/2 ReadAt:2/2 Seek:2/2 Size:0/1', }, 'io/fs': { - FS: 'Open', - File: 'Close Read Stat', - DirEntry: 'Info IsDir Name Type', - FileInfo: 'IsDir ModTime Mode Name Size Sys', - ReadDirFile: 'Close Read ReadDir Stat', - ReadDirFS: 'Open ReadDir', - ReadFileFS: 'Open ReadFile', - StatFS: 'Open Stat', - SubFS: 'Open Sub', - GlobFS: 'Glob Open', + FS: 'Open:1/2', + File: 'Close:0/1 Read:1/2 Stat:0/2', + DirEntry: 'Info:0/2 IsDir:0/1 Name:0/1 Type:0/1', + FileInfo: 'IsDir:0/1 ModTime:0/1 Mode:0/1 Name:0/1 Size:0/1 Sys:0/1', + ReadDirFile: 'Close:0/1 Read:1/2 ReadDir:1/2 Stat:0/2', + ReadDirFS: 'Open:1/2 ReadDir:1/2', + ReadFileFS: 'Open:1/2 ReadFile:1/2', + StatFS: 'Open:1/2 Stat:1/2', + SubFS: 'Open:1/2 Sub:1/2', + GlobFS: 'Glob:1/2 Open:1/2', }, os: { - File: 'Chdir Chmod Chown Close Fd Name PollFD Read ReadAt ReadDir ReadFrom Readdir Readdirnames Seek SetDeadline SetReadDeadline SetWriteDeadline Stat Sync SyscallConn Truncate Write WriteAt WriteString WriteTo', - FileInfo: 'IsDir ModTime Mode Name Size Sys', - DirEntry: 'Info IsDir Name Type', - Signal: 'Signal String', + File: 'Chdir:0/1 Chmod:1/1 Chown:2/1 Close:0/1 Fd:0/1 Name:0/1 PollFD:0/1 Read:1/2 ReadAt:2/2 ReadDir:1/2 ReadFrom:1/2 Readdir:1/2 Readdirnames:1/2 Seek:2/2 SetDeadline:1/1 SetReadDeadline:1/1 SetWriteDeadline:1/1 Stat:0/2 Sync:0/1 SyscallConn:0/2 Truncate:1/1 Write:1/2 WriteAt:2/2 WriteString:1/2 WriteTo:1/2', + FileInfo: 'IsDir:0/1 ModTime:0/1 Mode:0/1 Name:0/1 Size:0/1 Sys:0/1', + DirEntry: 'Info:0/2 IsDir:0/1 Name:0/1 Type:0/1', + Signal: 'Signal:0/0 String:0/1', }, fmt: { - Stringer: 'String', - GoStringer: 'GoString', - Formatter: 'Format', - State: 'Flag Precision Width Write', - Scanner: 'Scan', - ScanState: 'Read ReadRune SkipSpace Token UnreadRune Width', + Stringer: 'String:0/1', + GoStringer: 'GoString:0/1', + Formatter: 'Format:2/0', + State: 'Flag:1/1 Precision:0/2 Width:0/2 Write:1/2', + Scanner: 'Scan:2/1', + ScanState: 'Read:1/2 ReadRune:0/3 SkipSpace:0/0 Token:2/2 UnreadRune:0/1 Width:0/2', }, sort: { - Interface: 'Len Less Swap', + Interface: 'Len:0/1 Less:2/1 Swap:2/0', }, 'container/heap': { - Interface: 'Len Less Pop Push Swap', + Interface: 'Len:0/1 Less:2/1 Pop:0/1 Push:1/0 Swap:2/0', }, context: { - Context: 'Deadline Done Err Value', + Context: 'Deadline:0/2 Done:0/1 Err:0/1 Value:1/1', }, sync: { - Locker: 'Lock Unlock', - Mutex: 'Lock TryLock Unlock', - RWMutex: 'Lock RLock RLocker RUnlock TryLock TryRLock Unlock', + Locker: 'Lock:0/0 Unlock:0/0', + Mutex: 'Lock:0/0 TryLock:0/1 Unlock:0/0', + RWMutex: 'Lock:0/0 RLock:0/0 RLocker:0/1 RUnlock:0/0 TryLock:0/1 TryRLock:0/1 Unlock:0/0', }, net: { - Conn: 'Close LocalAddr Read RemoteAddr SetDeadline SetReadDeadline SetWriteDeadline Write', - PacketConn: 'Close LocalAddr ReadFrom SetDeadline SetReadDeadline SetWriteDeadline WriteTo', - Listener: 'Accept Addr Close', - Addr: 'Network String', - Error: 'Error Temporary Timeout', - TCPConn: 'Close CloseRead CloseWrite File LocalAddr MultipathTCP Read ReadFrom RemoteAddr SetDeadline SetKeepAlive SetKeepAliveConfig SetKeepAlivePeriod SetLinger SetNoDelay SetReadBuffer SetReadDeadline SetWriteBuffer SetWriteDeadline SyscallConn Write WriteTo', - UDPConn: 'Close File LocalAddr Read ReadFrom ReadFromUDP ReadFromUDPAddrPort ReadMsgUDP ReadMsgUDPAddrPort RemoteAddr SetDeadline SetReadBuffer SetReadDeadline SetWriteBuffer SetWriteDeadline SyscallConn Write WriteMsgUDP WriteMsgUDPAddrPort WriteTo WriteToUDP WriteToUDPAddrPort', - UnixConn: 'Close CloseRead CloseWrite File LocalAddr Read ReadFrom ReadFromUnix ReadMsgUnix RemoteAddr SetDeadline SetReadBuffer SetReadDeadline SetWriteBuffer SetWriteDeadline SyscallConn Write WriteMsgUnix WriteTo WriteToUnix', - TCPListener: 'Accept AcceptTCP Addr Close File SetDeadline SyscallConn', - UnixListener: 'Accept AcceptUnix Addr Close File SetDeadline SetUnlinkOnClose SyscallConn', + Conn: 'Close:0/1 LocalAddr:0/1 Read:1/2 RemoteAddr:0/1 SetDeadline:1/1 SetReadDeadline:1/1 SetWriteDeadline:1/1 Write:1/2', + PacketConn: 'Close:0/1 LocalAddr:0/1 ReadFrom:1/3 SetDeadline:1/1 SetReadDeadline:1/1 SetWriteDeadline:1/1 WriteTo:2/2', + Listener: 'Accept:0/2 Addr:0/1 Close:0/1', + Addr: 'Network:0/1 String:0/1', + Error: 'Error:0/1 Temporary:0/1 Timeout:0/1', + TCPConn: 'Close:0/1 CloseRead:0/1 CloseWrite:0/1 File:0/2 LocalAddr:0/1 MultipathTCP:0/2 Read:1/2 ReadFrom:1/2 RemoteAddr:0/1 SetDeadline:1/1 SetKeepAlive:1/1 SetKeepAliveConfig:1/1 SetKeepAlivePeriod:1/1 SetLinger:1/1 SetNoDelay:1/1 SetReadBuffer:1/1 SetReadDeadline:1/1 SetWriteBuffer:1/1 SetWriteDeadline:1/1 SyscallConn:0/2 Write:1/2 WriteTo:1/2', + UDPConn: 'Close:0/1 File:0/2 LocalAddr:0/1 Read:1/2 ReadFrom:1/3 ReadFromUDP:1/3 ReadFromUDPAddrPort:1/3 ReadMsgUDP:2/5 ReadMsgUDPAddrPort:2/5 RemoteAddr:0/1 SetDeadline:1/1 SetReadBuffer:1/1 SetReadDeadline:1/1 SetWriteBuffer:1/1 SetWriteDeadline:1/1 SyscallConn:0/2 Write:1/2 WriteMsgUDP:3/3 WriteMsgUDPAddrPort:3/3 WriteTo:2/2 WriteToUDP:2/2 WriteToUDPAddrPort:2/2', + UnixConn: 'Close:0/1 CloseRead:0/1 CloseWrite:0/1 File:0/2 LocalAddr:0/1 Read:1/2 ReadFrom:1/3 ReadFromUnix:1/3 ReadMsgUnix:2/5 RemoteAddr:0/1 SetDeadline:1/1 SetReadBuffer:1/1 SetReadDeadline:1/1 SetWriteBuffer:1/1 SetWriteDeadline:1/1 SyscallConn:0/2 Write:1/2 WriteMsgUnix:3/3 WriteTo:2/2 WriteToUnix:2/2', + TCPListener: 'Accept:0/2 AcceptTCP:0/2 Addr:0/1 Close:0/1 File:0/2 SetDeadline:1/1 SyscallConn:0/2', + UnixListener: 'Accept:0/2 AcceptUnix:0/2 Addr:0/1 Close:0/1 File:0/2 SetDeadline:1/1 SetUnlinkOnClose:1/0 SyscallConn:0/2', }, 'net/http': { - ResponseWriter: 'Header Write WriteHeader', - Hijacker: 'Hijack', - Flusher: 'Flush', - CloseNotifier: 'CloseNotify', - Pusher: 'Push', - Handler: 'ServeHTTP', - RoundTripper: 'RoundTrip', - File: 'Close Read Readdir Seek Stat', - FileSystem: 'Open', - CookieJar: 'Cookies SetCookies', - ServeMux: 'Handle HandleFunc Handler ServeHTTP', - Transport: 'CancelRequest Clone CloseIdleConnections NewClientConn RegisterProtocol RoundTrip', + ResponseWriter: 'Header:0/1 Write:1/2 WriteHeader:1/0', + Hijacker: 'Hijack:0/3', + Flusher: 'Flush:0/0', + CloseNotifier: 'CloseNotify:0/1', + Pusher: 'Push:2/1', + Handler: 'ServeHTTP:2/0', + RoundTripper: 'RoundTrip:1/2', + File: 'Close:0/1 Read:1/2 Readdir:1/2 Seek:2/2 Stat:0/2', + FileSystem: 'Open:1/2', + CookieJar: 'Cookies:1/1 SetCookies:2/0', + ServeMux: 'Handle:2/0 HandleFunc:2/0 Handler:1/2 ServeHTTP:2/0', + Transport: 'CancelRequest:1/0 Clone:0/1 CloseIdleConnections:0/0 NewClientConn:3/2 RegisterProtocol:2/0 RoundTrip:1/2', }, 'net/http/httptest': { - ResponseRecorder: 'Flush Header Result Write WriteHeader WriteString', + ResponseRecorder: 'Flush:0/0 Header:0/1 Result:0/1 Write:1/2 WriteHeader:1/0 WriteString:1/2', }, bufio: { - Reader: 'Buffered Discard Peek Read ReadByte ReadBytes ReadLine ReadRune ReadSlice ReadString Reset Size UnreadByte UnreadRune WriteTo', - Writer: 'Available AvailableBuffer Buffered Flush ReadFrom Reset Size Write WriteByte WriteRune WriteString', - ReadWriter: 'Available AvailableBuffer Buffered Discard Flush Peek Read ReadByte ReadBytes ReadFrom ReadLine ReadRune ReadSlice ReadString Reset Size UnreadByte UnreadRune Write WriteByte WriteRune WriteString WriteTo', + Reader: 'Buffered:0/1 Discard:1/2 Peek:1/2 Read:1/2 ReadByte:0/2 ReadBytes:1/2 ReadLine:0/3 ReadRune:0/3 ReadSlice:1/2 ReadString:1/2 Reset:1/0 Size:0/1 UnreadByte:0/1 UnreadRune:0/1 WriteTo:1/2', + Writer: 'Available:0/1 AvailableBuffer:0/1 Buffered:0/1 Flush:0/1 ReadFrom:1/2 Reset:1/0 Size:0/1 Write:1/2 WriteByte:1/1 WriteRune:1/2 WriteString:1/2', + ReadWriter: 'Available:0/1 AvailableBuffer:0/1 Buffered:0/1 Discard:1/2 Flush:0/1 Peek:1/2 Read:1/2 ReadByte:0/2 ReadBytes:1/2 ReadFrom:1/2 ReadLine:0/3 ReadRune:0/3 ReadSlice:1/2 ReadString:1/2 Reset:1/0 Size:0/1 UnreadByte:0/1 UnreadRune:0/1 Write:1/2 WriteByte:1/1 WriteRune:1/2 WriteString:1/2 WriteTo:1/2', }, bytes: { - Buffer: 'Available AvailableBuffer Bytes Cap Grow Len Next Peek Read ReadByte ReadBytes ReadFrom ReadRune ReadString Reset String Truncate UnreadByte UnreadRune Write WriteByte WriteRune WriteString WriteTo', - Reader: 'Len Read ReadAt ReadByte ReadRune Reset Seek Size UnreadByte UnreadRune WriteTo', + Buffer: 'Available:0/1 AvailableBuffer:0/1 Bytes:0/1 Cap:0/1 Grow:1/0 Len:0/1 Next:1/1 Peek:1/2 Read:1/2 ReadByte:0/2 ReadBytes:1/2 ReadFrom:1/2 ReadRune:0/3 ReadString:1/2 Reset:0/0 String:0/1 Truncate:1/0 UnreadByte:0/1 UnreadRune:0/1 Write:1/2 WriteByte:1/1 WriteRune:1/2 WriteString:1/2 WriteTo:1/2', + Reader: 'Len:0/1 Read:1/2 ReadAt:2/2 ReadByte:0/2 ReadRune:0/3 Reset:1/0 Seek:2/2 Size:0/1 UnreadByte:0/1 UnreadRune:0/1 WriteTo:1/2', }, strings: { - Reader: 'Len Read ReadAt ReadByte ReadRune Reset Seek Size UnreadByte UnreadRune WriteTo', - Builder: 'Cap Grow Len Reset String Write WriteByte WriteRune WriteString', + Reader: 'Len:0/1 Read:1/2 ReadAt:2/2 ReadByte:0/2 ReadRune:0/3 Reset:1/0 Seek:2/2 Size:0/1 UnreadByte:0/1 UnreadRune:0/1 WriteTo:1/2', + Builder: 'Cap:0/1 Grow:1/0 Len:0/1 Reset:0/0 String:0/1 Write:1/2 WriteByte:1/1 WriteRune:1/2 WriteString:1/2', }, testing: { - TB: 'ArtifactDir Attr Chdir Cleanup Context Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Name Output Setenv Skip SkipNow Skipf Skipped TempDir private', - T: 'ArtifactDir Attr Chdir Cleanup Context Deadline Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Name Output Parallel Run Setenv Skip SkipNow Skipf Skipped TempDir private', - B: 'ArtifactDir Attr Chdir Cleanup Context Elapsed Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Loop Name Output ReportAllocs ReportMetric ResetTimer Run RunParallel SetBytes SetParallelism Setenv Skip SkipNow Skipf Skipped StartTimer StopTimer TempDir private', - F: 'Add ArtifactDir Attr Chdir Cleanup Context Error Errorf Fail FailNow Failed Fatal Fatalf Fuzz Helper Log Logf Name Output Setenv Skip SkipNow Skipf Skipped TempDir private', + TB: 'ArtifactDir:0/1 Attr:2/0 Chdir:1/0 Cleanup:1/0 Context:0/1 Error:1/0 Errorf:2/0 Fail:0/0 FailNow:0/0 Failed:0/1 Fatal:1/0 Fatalf:2/0 Helper:0/0 Log:1/0 Logf:2/0 Name:0/1 Output:0/1 Setenv:2/0 Skip:1/0 SkipNow:0/0 Skipf:2/0 Skipped:0/1 TempDir:0/1 private:0/0', + T: 'ArtifactDir:0/1 Attr:2/0 Chdir:1/0 Cleanup:1/0 Context:0/1 Deadline:0/2 Error:1/0 Errorf:2/0 Fail:0/0 FailNow:0/0 Failed:0/1 Fatal:1/0 Fatalf:2/0 Helper:0/0 Log:1/0 Logf:2/0 Name:0/1 Output:0/1 Parallel:0/0 Run:2/1 Setenv:2/0 Skip:1/0 SkipNow:0/0 Skipf:2/0 Skipped:0/1 TempDir:0/1 private:0/0', + B: 'ArtifactDir:0/1 Attr:2/0 Chdir:1/0 Cleanup:1/0 Context:0/1 Elapsed:0/1 Error:1/0 Errorf:2/0 Fail:0/0 FailNow:0/0 Failed:0/1 Fatal:1/0 Fatalf:2/0 Helper:0/0 Log:1/0 Logf:2/0 Loop:0/1 Name:0/1 Output:0/1 ReportAllocs:0/0 ReportMetric:2/0 ResetTimer:0/0 Run:2/1 RunParallel:1/0 SetBytes:1/0 SetParallelism:1/0 Setenv:2/0 Skip:1/0 SkipNow:0/0 Skipf:2/0 Skipped:0/1 StartTimer:0/0 StopTimer:0/0 TempDir:0/1 private:0/0', + F: 'Add:1/0 ArtifactDir:0/1 Attr:2/0 Chdir:1/0 Cleanup:1/0 Context:0/1 Error:1/0 Errorf:2/0 Fail:0/0 FailNow:0/0 Failed:0/1 Fatal:1/0 Fatalf:2/0 Fuzz:1/0 Helper:0/0 Log:1/0 Logf:2/0 Name:0/1 Output:0/1 Setenv:2/0 Skip:1/0 SkipNow:0/0 Skipf:2/0 Skipped:0/1 TempDir:0/1 private:0/0', }, 'log/slog': { - Handler: 'Enabled Handle WithAttrs WithGroup', - JSONHandler: 'Enabled Handle WithAttrs WithGroup', - TextHandler: 'Enabled Handle WithAttrs WithGroup', + Handler: 'Enabled:2/1 Handle:2/1 WithAttrs:1/1 WithGroup:1/1', + JSONHandler: 'Enabled:2/1 Handle:2/1 WithAttrs:1/1 WithGroup:1/1', + TextHandler: 'Enabled:2/1 Handle:2/1 WithAttrs:1/1 WithGroup:1/1', }, encoding: { - BinaryMarshaler: 'MarshalBinary', - BinaryUnmarshaler: 'UnmarshalBinary', - TextMarshaler: 'MarshalText', - TextUnmarshaler: 'UnmarshalText', + BinaryMarshaler: 'MarshalBinary:0/2', + BinaryUnmarshaler: 'UnmarshalBinary:1/1', + TextMarshaler: 'MarshalText:0/2', + TextUnmarshaler: 'UnmarshalText:1/1', }, 'encoding/json': { - Marshaler: 'MarshalJSON', - Unmarshaler: 'UnmarshalJSON', - RawMessage: 'MarshalJSON UnmarshalJSON', + Marshaler: 'MarshalJSON:0/2', + Unmarshaler: 'UnmarshalJSON:1/1', + RawMessage: 'MarshalJSON:0/2 UnmarshalJSON:1/1', }, 'encoding/xml': { - Marshaler: 'MarshalXML', - Unmarshaler: 'UnmarshalXML', + Marshaler: 'MarshalXML:2/1', + Unmarshaler: 'UnmarshalXML:2/1', }, 'encoding/gob': { - GobEncoder: 'GobEncode', - GobDecoder: 'GobDecode', + GobEncoder: 'GobEncode:0/2', + GobDecoder: 'GobDecode:1/1', }, expvar: { - Var: 'String', + Var: 'String:0/1', }, hash: { - Hash: 'BlockSize Reset Size Sum Write', - Hash32: 'BlockSize Reset Size Sum Sum32 Write', - Hash64: 'BlockSize Reset Size Sum Sum64 Write', + Hash: 'BlockSize:0/1 Reset:0/0 Size:0/1 Sum:1/1 Write:1/2', + Hash32: 'BlockSize:0/1 Reset:0/0 Size:0/1 Sum:1/1 Sum32:0/1 Write:1/2', + Hash64: 'BlockSize:0/1 Reset:0/0 Size:0/1 Sum:1/1 Sum64:0/1 Write:1/2', }, crypto: { - Signer: 'Public Sign', - Decrypter: 'Decrypt Public', + Signer: 'Public:0/1 Sign:3/2', + Decrypter: 'Decrypt:3/2 Public:0/1', }, 'crypto/elliptic': { - Curve: 'Add Double IsOnCurve Params ScalarBaseMult ScalarMult', + Curve: 'Add:4/2 Double:2/2 IsOnCurve:2/1 Params:0/1 ScalarBaseMult:1/2 ScalarMult:3/2', }, 'crypto/cipher': { - Block: 'BlockSize Decrypt Encrypt', - Stream: 'XORKeyStream', - AEAD: 'NonceSize Open Overhead Seal', - BlockMode: 'BlockSize CryptBlocks', + Block: 'BlockSize:0/1 Decrypt:2/0 Encrypt:2/0', + Stream: 'XORKeyStream:2/0', + AEAD: 'NonceSize:0/1 Open:4/2 Overhead:0/1 Seal:4/1', + BlockMode: 'BlockSize:0/1 CryptBlocks:2/0', }, 'crypto/tls': { - Conn: 'Close CloseWrite ConnectionState Handshake HandshakeContext LocalAddr NetConn OCSPResponse Read RemoteAddr SetDeadline SetReadDeadline SetWriteDeadline VerifyHostname Write', + Conn: 'Close:0/1 CloseWrite:0/1 ConnectionState:0/1 Handshake:0/1 HandshakeContext:1/1 LocalAddr:0/1 NetConn:0/1 OCSPResponse:0/1 Read:1/2 RemoteAddr:0/1 SetDeadline:1/1 SetReadDeadline:1/1 SetWriteDeadline:1/1 VerifyHostname:1/1 Write:1/2', }, image: { - Image: 'At Bounds ColorModel', - PalettedImage: 'At Bounds ColorIndexAt ColorModel', + Image: 'At:2/1 Bounds:0/1 ColorModel:0/1', + PalettedImage: 'At:2/1 Bounds:0/1 ColorIndexAt:2/1 ColorModel:0/1', }, 'image/color': { - Color: 'RGBA', - Model: 'Convert', + Color: 'RGBA:0/4', + Model: 'Convert:1/1', }, 'database/sql': { - Scanner: 'Scan', + Scanner: 'Scan:1/1', }, 'database/sql/driver': { - Valuer: 'Value', - Driver: 'Open', - Conn: 'Begin Close Prepare', - Stmt: 'Close Exec NumInput Query', - Tx: 'Commit Rollback', - Rows: 'Close Columns Next', - Result: 'LastInsertId RowsAffected', + Valuer: 'Value:0/2', + Driver: 'Open:1/2', + Conn: 'Begin:0/2 Close:0/1 Prepare:1/2', + Stmt: 'Close:0/1 Exec:1/2 NumInput:0/1 Query:1/2', + Tx: 'Commit:0/1 Rollback:0/1', + Rows: 'Close:0/1 Columns:0/1 Next:1/1', + Result: 'LastInsertId:0/2 RowsAffected:0/2', }, flag: { - Value: 'Set String', - Getter: 'Get Set String', + Value: 'Set:1/1 String:0/1', + Getter: 'Get:0/1 Set:1/1 String:0/1', }, runtime: { - Error: 'Error RuntimeError', + Error: 'Error:0/1 RuntimeError:0/0', }, reflect: { - Type: 'Align AssignableTo Bits CanSeq CanSeq2 ChanDir Comparable ConvertibleTo Elem Field FieldAlign FieldByIndex FieldByName FieldByNameFunc Fields Implements In Ins IsVariadic Key Kind Len Method MethodByName Methods Name NumField NumIn NumMethod NumOut Out Outs OverflowComplex OverflowFloat OverflowInt OverflowUint PkgPath Size String common uncommon', + Type: 'Align:0/1 AssignableTo:1/1 Bits:0/1 CanSeq:0/1 CanSeq2:0/1 ChanDir:0/1 Comparable:0/1 ConvertibleTo:1/1 Elem:0/1 Field:1/1 FieldAlign:0/1 FieldByIndex:1/1 FieldByName:1/2 FieldByNameFunc:1/2 Fields:0/1 Implements:1/1 In:1/1 Ins:0/1 IsVariadic:0/1 Key:0/1 Kind:0/1 Len:0/1 Method:1/1 MethodByName:1/2 Methods:0/1 Name:0/1 NumField:0/1 NumIn:0/1 NumMethod:0/1 NumOut:0/1 Out:1/1 Outs:0/1 OverflowComplex:1/1 OverflowFloat:1/1 OverflowInt:1/1 OverflowUint:1/1 PkgPath:0/1 Size:0/1 String:0/1 common:0/1 uncommon:0/1', }, time: { - Time: 'Add AddDate After AppendBinary AppendFormat AppendText Before Clock Compare Date Day Equal Format GoString GobDecode GobEncode Hour ISOWeek In IsDST IsZero Local Location MarshalBinary MarshalJSON MarshalText Minute Month Nanosecond Round Second String Sub Truncate UTC Unix UnixMicro UnixMilli UnixNano UnmarshalBinary UnmarshalJSON UnmarshalText Weekday Year YearDay Zone ZoneBounds', + Time: 'Add:1/1 AddDate:3/1 After:1/1 AppendBinary:1/2 AppendFormat:2/1 AppendText:1/2 Before:1/1 Clock:0/3 Compare:1/1 Date:0/3 Day:0/1 Equal:1/1 Format:1/1 GoString:0/1 GobDecode:1/1 GobEncode:0/2 Hour:0/1 ISOWeek:0/2 In:1/1 IsDST:0/1 IsZero:0/1 Local:0/1 Location:0/1 MarshalBinary:0/2 MarshalJSON:0/2 MarshalText:0/2 Minute:0/1 Month:0/1 Nanosecond:0/1 Round:1/1 Second:0/1 String:0/1 Sub:1/1 Truncate:1/1 UTC:0/1 Unix:0/1 UnixMicro:0/1 UnixMilli:0/1 UnixNano:0/1 UnmarshalBinary:1/1 UnmarshalJSON:1/1 UnmarshalText:1/1 Weekday:0/1 Year:0/1 YearDay:0/1 Zone:0/2 ZoneBounds:0/2', }, 'compress/gzip': { - Reader: 'Close Multistream Read Reset', - Writer: 'Close Flush Reset Write', + Reader: 'Close:0/1 Multistream:1/0 Read:1/2 Reset:1/1', + Writer: 'Close:0/1 Flush:0/1 Reset:1/0 Write:1/2', }, }; +/** A standard-library type's methods: each name and its `params/results`. */ +export type GoStdlibMethods = ReadonlyMap; + /** `error`, the one predeclared type with a method. */ -const GO_ERROR_METHODS: ReadonlySet = new Set(['Error']); +const GO_ERROR_METHODS: GoStdlibMethods = new Map([['Error', '0/1']]); -const parsed = new Map | null>(); +const parsed = new Map(); /** - * The method names of the standard-library type `typeName` that the package - * at `importPath` declares, or of the predeclared `error` when `importPath` - * is null. Undefined for a type this table does not know. + * The methods of the standard-library type `typeName` that the package at + * `importPath` declares, or of the predeclared `error` when `importPath` is + * null. Undefined for a type this table does not know. */ -export function goStdlibMethods(importPath: string | null, typeName: string): ReadonlySet | undefined { +export function goStdlibMethods(importPath: string | null, typeName: string): GoStdlibMethods | undefined { if (importPath === null) return typeName === 'error' ? GO_ERROR_METHODS : undefined; const key = `${importPath}.${typeName}`; let methods = parsed.get(key); if (methods === undefined) { - const names = Object.prototype.hasOwnProperty.call(GO_STDLIB_METHODS, importPath) + const listed = Object.prototype.hasOwnProperty.call(GO_STDLIB_METHODS, importPath) ? GO_STDLIB_METHODS[importPath]![typeName] : undefined; - methods = typeof names === 'string' ? new Set(names.split(' ')) : null; + methods = typeof listed === 'string' + ? new Map(listed.split(' ').map((entry) => entry.split(':') as [string, string])) + : null; parsed.set(key, methods); } return methods ?? undefined; From dd9513ce201991186dfd4ec2d71aa281779ddfe8 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 21 Oct 2026 12:43:25 -0500 Subject: [PATCH 4/6] fix(go): a standard-library method counts like any promoted one, at its counts With the table's arities, keeping a standard-library method from answering a method the project's interface declares only cost real implementers: an embedded `*testing.T` is a TBRun, a `sync.Mutex` hugo's `Locker`, a `*net.TCPConn` caddy's `canSetKeepAliveConfig`. The arity gate already keeps the look-alikes that rule was for: etcd's gRPC `LockClient.Lock` takes a context and a request, grpc-go's binarylog `Sink.Write` a log entry, prometheus's `Discoverer.Run` a channel. Measured on 11 repos, the 63 edges it adds all hold in Go. So a type's method set is one set of keys again, its own methods, its embeddings' and the standard-library ones', plus the undescribed embeddings it needs. Co-Authored-By: Claude Opus 5.5 --- __tests__/go-implements-stdlib.test.ts | 36 +++++++++--- src/resolution/callback-synthesizer.ts | 80 +++++++++++--------------- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/__tests__/go-implements-stdlib.test.ts b/__tests__/go-implements-stdlib.test.ts index b7ab75242b..35255c1d69 100644 --- a/__tests__/go-implements-stdlib.test.ts +++ b/__tests__/go-implements-stdlib.test.ts @@ -13,8 +13,10 @@ * * The methods of the standard-library types projects embed are known now, * with their parameter and result counts, and `error` is recorded as the - * embedding it is. Such a method answers only what the standard library asks - * for: an embedded `sync.Mutex` gives no struct etcd's gRPC `LockClient.Lock`. + * embedding it is. Such a method counts like any other, at its counts: an + * embedded `sync.Mutex` makes a struct hugo's `Locker`, but not etcd's gRPC + * `LockClient`, whose `Lock` takes a context and a request, and a `Write` that + * takes a record is no `io.Writer`'s. * An unexported one is its own package's, as Go has it: `testing.TB`'s * `private`, which no project type can declare. Embedding any other outside * type (`zapcore.Core`, `grpc.ClientStream`) asks for methods nothing names: @@ -75,6 +77,12 @@ type tRunner struct{ *testing.T } func (r tRunner) Run(name string, f func(*testing.T)) bool { return r.T.Run(name, f) } +// discovery/file's test helper: all of TBRun through *testing.T, Run included. +type testRunner struct { + *testing.T + dir string +} + type syncWriter interface { io.Writer Sync() error @@ -165,7 +173,13 @@ type LockClient interface { Unlock(ctx context.Context, key string) error } -// A mutex's Lock and Unlock are not those. +// hugo's: what a mutex has. +type Locker interface { + Lock() + Unlock() +} + +// A mutex's Lock and Unlock: a Locker's, not a LockClient's. type waitRecorder struct { sync.Mutex actions []string @@ -355,12 +369,17 @@ describe('Go implicit satisfaction knows what standard-library embeddings bring' 'errs.statusErr -> errs.coded', 'fileutil.FileBufReader -> fileutil.FileReader', 'fileutil.fileReader -> fileutil.FileReader', + // An *os.File is what prometheus's SegmentFile and syncWriter describe. + 'fileutil.fileReader -> promql.SegmentFile', + 'fileutil.fileReader -> promql.syncWriter', 'fileutil.lockProxy -> fileutil.LockClient', + 'fileutil.waitRecorder -> fileutil.Locker', 'gin.responseWriter -> gin.ResponseWriter', 'promql.JSONFileLogger -> promql.QueryLogger', 'promql.Segment -> promql.syncWriter', 'promql.queryLogFile -> promql.syncWriter', 'promql.tRunner -> promql.TBRun', + 'promql.testRunner -> promql.TBRun', ]); }); @@ -368,13 +387,15 @@ describe('Go implicit satisfaction knows what standard-library embeddings bring' const implementers = (name: string, file: string) => (buildTypeHierarchy(cg, one(name, file))?.descendants ?? []).map((d) => `${d.relation} ${pkg(d.node)}`).sort(); // Not the discoverer with a Run, nor fakeTB, whose private is its own package's. - expect(implementers('TBRun', 'promql/promql.go')).toEqual(['implements promql.tRunner']); + expect(implementers('TBRun', 'promql/promql.go')).toEqual(['implements promql.tRunner', 'implements promql.testRunner']); // Not walLog, whose Write takes a record and returns only an error. expect(implementers('syncWriter', 'promql/promql.go')).toEqual([ + 'implements fileutil.fileReader', 'implements promql.Segment', 'implements promql.queryLogFile', ]); - // A call through TBRun reaches tRunner's Run, not the discoverer's. + // A call through TBRun reaches tRunner's Run, not the discoverer's; + // testRunner's is testing's, outside the graph. const run = cg .getOutgoingEdgesFrom([one('TBRun', 'promql/promql.go').id], ['contains']) .map((e) => cg.getNode(e.target)) @@ -397,14 +418,15 @@ describe('Go implicit satisfaction knows what standard-library embeddings bring' } }); - it('answers a method only the project asks for with one the project declares', () => { + it('holds a method a standard-library embedding brings to the counts of the one it answers', () => { const into = (name: string, file: string) => cg .getIncomingEdgesTo([one(name, file).id], ['implements']) .filter((e) => meta(e).synthesizedBy === 'go-implements') .map((e) => pkg(cg.getNode(e.source))) .sort(); - // waitRecorder's Lock and Unlock are sync.Mutex's. + // waitRecorder's Lock and Unlock are sync.Mutex's: they take nothing. + expect(into('Locker', 'fileutil/fileutil.go')).toEqual(['fileutil.waitRecorder']); expect(into('LockClient', 'fileutil/fileutil.go')).toEqual(['fileutil.lockProxy']); // responseWriter's Header is http.ResponseWriter's, which ResponseWriter asks for. expect(into('ResponseWriter', 'gin/response_writer.go')).toEqual(['gin.responseWriter']); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index e53d5b3748..fa7ed1da70 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -852,12 +852,14 @@ function goMethodKey(m: Node): string { function goStdlibMethodKey(importPath: string | null, name: string): string { return GO_EXPORTED_NAME.test(name) ? name : `std:${importPath ?? ''}\0${name}`; } -/** A Go type's method keys ({@link goMethodKey}), by where each is known from. */ +/** What a Go type has, or an interface asks for. */ interface GoMethodNames { - /** Declared in the project: the type's own, and those of the project types it embeds. */ - readonly declared: Set; - /** Those of the standard-library types it embeds, and `error`'s (see ./go-stdlib-methods). */ - readonly stdlib: ReadonlySet; + /** + * Its methods by {@link goMethodKey}: its own, the project types' it + * embeds, and those the standard-library types it embeds bring + * (./go-stdlib-methods), `error`'s included. + */ + readonly methods: Set; /** Embedded types from outside the index whose methods nothing names, by package and name. */ readonly opaque: ReadonlySet; } @@ -980,14 +982,14 @@ function goGrpcServerArity(signature: string | undefined): string | null { * * A type embedded from outside the index leaves only the resolver's failed * `extends` row. The methods of a standard-library type, and of `error`, are - * known (./go-stdlib-methods): prometheus's `TBRun` is all of `testing.TB` - * plus `Run`, not any struct with a `Run`. Such a method answers only one the - * standard library asks for too: etcd's gRPC `LockClient.Lock` is not the - * `Lock` an embedded `sync.Mutex` promotes. Any other outside type's methods - * are unknown, so a struct embedding the same type has them, and otherwise - * only the one type with every method the interface does name is taken to - * implement it — kapacitor's `Diagnostic` (a third-party `Logger` plus `Log`) - * has no more claim on every struct with a `Log` than on none of them. + * known (./go-stdlib-methods), with their counts: prometheus's `TBRun` is all + * of `testing.TB` plus `Run`, not any struct with a `Run`, and a struct + * embedding `sync.Mutex` is hugo's `Locker` but not etcd's gRPC `LockClient`, + * whose `Lock` takes a context and a request. Any other outside type's + * methods are unknown, so a struct embedding the same type has them, and + * otherwise only the one type with every method the interface does name is + * taken to implement it — kapacitor's `Diagnostic` (a third-party `Logger` + * plus `Log`) has no more claim on every struct with a `Log` than on none. * * Methods match by name, and an unexported name by its package too * ({@link goMethodKey}). prometheus has a `refresher { refresh(…) }` in each of @@ -1124,33 +1126,27 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, } await onYield(); - // Own methods plus every embedded type's, by where each is known from. - const NONE_KNOWN: ReadonlySet = new Set(); + // Own methods plus every embedded type's, a standard-library one's too. + const NONE_OPAQUE: ReadonlySet = new Set(); const methodNames = (id: string): GoMethodNames => { const types = embedClosure(id); - if (types.length === 1 && !outside.has(id)) return { declared: ownMethods(id), stdlib: NONE_KNOWN, opaque: NONE_KNOWN }; - const declared = new Set(); - const stdlib = new Set(); + if (types.length === 1 && !outside.has(id)) return { methods: ownMethods(id), opaque: NONE_OPAQUE }; + const methods = new Set(); const opaque = new Set(); for (const t of types) { - for (const m of ownMethods(t)) declared.add(m); + for (const m of ownMethods(t)) methods.add(m); const out = outside.get(t); if (!out) continue; - for (const known of out.stdlib) for (const key of known.keys()) stdlib.add(key); + for (const known of out.stdlib) for (const key of known.keys()) methods.add(key); for (const type of out.opaque) opaque.add(type); } - return { declared, stdlib, opaque }; + return { methods, opaque }; }; - // Every method `want` names, wherever it is known from: a method the - // standard library asks for is answered by any method of that key, and one - // only the project asks for by a method the project declares. - const coversNamed = (have: GoMethodNames, want: GoMethodNames): boolean => { - for (const m of want.declared) { - if (!have.declared.has(m) && !want.stdlib.has(m)) return false; - } - for (const m of want.stdlib) { - if (!have.declared.has(m) && !have.stdlib.has(m)) return false; + const covers = (have: Set, want: Set): boolean => { + if (have.size < want.size) return false; + for (const m of want) { + if (!have.has(m)) return false; } return true; }; @@ -1160,16 +1156,6 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, } return true; }; - const declaresAll = (own: Set, want: GoMethodNames): boolean => { - if (want.opaque.size > 0) return false; - for (const m of want.declared) { - if (!own.has(m)) return false; - } - for (const m of want.stdlib) { - if (!own.has(m)) return false; - } - return true; - }; // 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 @@ -1260,7 +1246,7 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, for (const s of goImplementers) { if ((++scanned255 & 63) === 0) await onYield(); const have = methodNames(s.id); - if (have.declared.size + have.stdlib.size + have.opaque.size === 0) continue; + if (have.methods.size + have.opaque.size === 0) continue; candidates.push(s); candidateMethods.set(s.id, have); } @@ -1268,8 +1254,7 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, for (const iface of goInterfaces) { if ((++scanned255 & 63) === 0) await onYield(); const want = methodNames(iface.id); - const named = want.stdlib.size === 0 ? want.declared.size : new Set([...want.declared, ...want.stdlib]).size; - if (named === 0 && want.opaque.size === 0) continue; // empty interface (`any`) — would match everything + if (want.methods.size + want.opaque.size === 0) continue; // empty interface (`any`) — would match everything let added = 0; const link = (s: Node): void => { const key = `${s.id}>${iface.id}`; @@ -1299,7 +1284,7 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, for (const s of candidates) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; const have = candidateMethods.get(s.id)!; - if (have.declared.size + have.stdlib.size < named || !coversNamed(have, want)) continue; + if (!covers(have.methods, want.methods)) continue; if (!wanted) wanted = wantedArities(iface); if (!aritiesFit(s.id, wanted)) continue; covering++; @@ -1308,10 +1293,11 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, if (covering === 1) sole = s; continue; } - if (declaresAll(ownMethods(s.id), want)) link(s); - else throughEmbedding.push(s); + const own = ownMethods(s.id); + if (want.opaque.size > 0 || (own !== have.methods && !covers(own, want.methods))) throughEmbedding.push(s); + else link(s); } - if (sole && covering === 1 && named > 0) throughEmbedding.push(sole); + if (sole && covering === 1 && want.methods.size > 0) throughEmbedding.push(sole); for (const s of throughEmbedding) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; link(s); From e215c56388e8d5099b5d34880654789edb5126a5 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 21 Oct 2026 12:45:37 -0500 Subject: [PATCH 5/6] docs(changelog): standard-library methods count with their counts, sync.Mutex included Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bec7363efd..1c880997eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,7 +158,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading. - In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. -- In Go, the methods a type gets from an embedded standard-library type, like `io.Closer`, `http.ResponseWriter`, `testing.TB` or `*os.File`, or from the built-in `error`, now count when working out which types implement an interface. Before, they were left out: prometheus's `TBRun`, which is all of `testing.TB` plus `Run`, was listed as implemented by its service discoveries because each has a `Run`, and an interface made only of standard-library ones, like `slog.Handler` plus `io.Closer`, had no implementations at all. An interface that embeds a type from another module, whose methods can't be seen, is now implemented only by a type that embeds the same one or by the only type with all its other methods, not by every type that shares a method name with it. Re-index Go projects after upgrading. +- In Go, the methods a type gets from an embedded standard-library type, like `io.Closer`, `http.ResponseWriter`, `testing.TB`, `*os.File` or `sync.Mutex`, or from the built-in `error`, now count when working out which types implement an interface, each with its parameters and results. Before, they were left out: prometheus's `TBRun`, which is all of `testing.TB` plus `Run`, was listed as implemented by a type that only has a `Run`, an interface made only of standard-library ones, like `slog.Handler` plus `io.Closer`, had no implementations at all, and a struct embedding `sync.Mutex` didn't implement an interface of your own that asks for `Lock` and `Unlock`. An interface that embeds a type from another module, whose methods can't be seen, is now implemented only by a type that embeds the same one or by the only type with all its other methods, not by every type that shares a method name with it. 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 Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading. From b2cec3ccd69b3d80689e44b35160eb26c1441a0b Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 21 Oct 2026 13:03:28 -0500 Subject: [PATCH 6/6] test(go): the struct-field fixture's embedded error is recorded as a supertype #2655's Embedder embeds `error`, which both extractors now record like any embedding from outside the index. Co-Authored-By: Claude Opus 5.5 --- __tests__/go-struct-field-refs.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/__tests__/go-struct-field-refs.test.ts b/__tests__/go-struct-field-refs.test.ts index 351f10841e..385d686051 100644 --- a/__tests__/go-struct-field-refs.test.ts +++ b/__tests__/go-struct-field-refs.test.ts @@ -260,6 +260,7 @@ describe('a Go struct references the types its fields are declared with', () => .map((r) => `${r.referenceKind} ${r.referenceName}`) .sort(); }; + // An embedded `error` is an embedding too: the one predeclared type with a method. expect(refsOf('Embedder')).toEqual([ 'extends Base', 'extends ClientWithList', @@ -267,6 +268,7 @@ describe('a Go struct references the types its fields are declared with', () => 'extends List', 'extends Pointer', 'extends Queryable', + 'extends error', 'references Pod', 'references PodList', ]);