diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ded831..0619badc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,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`, `*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. diff --git a/__tests__/go-implements-embedding.test.ts b/__tests__/go-implements-embedding.test.ts index 702a52f6..ca317090 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 00000000..35255c1d --- /dev/null +++ b/__tests__/go-implements-stdlib.test.ts @@ -0,0 +1,491 @@ +/** + * 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 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 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: + * 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 { 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. + '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) } + +// 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 +} + +// 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 } + +// 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 + 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 +} + +// 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 +} + +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 +} +`, + 'testing/fake.go': fakeTestingTB(), +}; + +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', + // 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', + ]); + }); + + 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', '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; + // 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)) + .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('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: 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']); + }); + + 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 2883c6f5..f1d1736c 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/__tests__/go-struct-field-refs.test.ts b/__tests__/go-struct-field-refs.test.ts index 351f1084..385d6860 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', ]); diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index 9b52e97a..71d7ba58 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -936,7 +936,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" { @@ -944,7 +944,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 331f9f48..5b02ba25 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -3027,6 +3027,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 7dae97d1..d3a51444 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 f54ce3c4..fa7ed1da 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,25 @@ 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}`; +} +/** What a Go type has, or an interface asks for. */ +interface GoMethodNames { + /** + * 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; +} /** * 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 +978,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), 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 @@ -973,10 +1003,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 +1016,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,13 +1105,42 @@ 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, 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) 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 { 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)) methods.add(m); + const out = outside.get(t); + if (!out) continue; + 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 { methods, opaque }; }; const covers = (have: Set, want: Set): boolean => { @@ -1089,11 +1150,18 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr } return true; }; + const coversOpaque = (have: GoMethodNames, want: GoMethodNames): boolean => { + for (const type of want.opaque) { + if (!have.opaque.has(type)) 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 +1182,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 +1213,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 +1239,22 @@ 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.methods.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); + 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}`; @@ -1195,20 +1274,30 @@ 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 (!covers(have.methods, want.methods)) continue; if (!wanted) wanted = wantedArities(iface); if (!aritiesFit(s.id, wanted)) continue; + covering++; + if (embeds(s.id).includes(iface.id)) continue; // declared by embedding it + if (!coversOpaque(have, want)) { + if (covering === 1) sole = s; + continue; + } const own = ownMethods(s.id); - if (own !== have && !covers(own, want)) throughEmbedding.push(s); + if (want.opaque.size > 0 || (own !== have.methods && !covers(own, want.methods))) throughEmbedding.push(s); else link(s); } + 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); @@ -5181,7 +5270,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 00000000..a2c85387 --- /dev/null +++ b/src/resolution/go-stdlib-methods.ts @@ -0,0 +1,253 @@ +/** + * 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 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 (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: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: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: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: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:0/1 Less:2/1 Swap:2/0', + }, + 'container/heap': { + Interface: 'Len:0/1 Less:2/1 Pop:0/1 Push:1/0 Swap:2/0', + }, + context: { + Context: 'Deadline:0/2 Done:0/1 Err:0/1 Value:1/1', + }, + sync: { + 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: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: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:0/0 Header:0/1 Result:0/1 Write:1/2 WriteHeader:1/0 WriteString:1/2', + }, + bufio: { + 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: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: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: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: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:0/2', + BinaryUnmarshaler: 'UnmarshalBinary:1/1', + TextMarshaler: 'MarshalText:0/2', + TextUnmarshaler: 'UnmarshalText:1/1', + }, + 'encoding/json': { + Marshaler: 'MarshalJSON:0/2', + Unmarshaler: 'UnmarshalJSON:1/1', + RawMessage: 'MarshalJSON:0/2 UnmarshalJSON:1/1', + }, + 'encoding/xml': { + Marshaler: 'MarshalXML:2/1', + Unmarshaler: 'UnmarshalXML:2/1', + }, + 'encoding/gob': { + GobEncoder: 'GobEncode:0/2', + GobDecoder: 'GobDecode:1/1', + }, + expvar: { + Var: 'String:0/1', + }, + hash: { + 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:0/1 Sign:3/2', + Decrypter: 'Decrypt:3/2 Public:0/1', + }, + 'crypto/elliptic': { + Curve: 'Add:4/2 Double:2/2 IsOnCurve:2/1 Params:0/1 ScalarBaseMult:1/2 ScalarMult:3/2', + }, + 'crypto/cipher': { + 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: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: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:0/4', + Model: 'Convert:1/1', + }, + 'database/sql': { + Scanner: 'Scan:1/1', + }, + 'database/sql/driver': { + 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:1/1 String:0/1', + Getter: 'Get:0/1 Set:1/1 String:0/1', + }, + runtime: { + Error: 'Error:0/1 RuntimeError:0/0', + }, + reflect: { + 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: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: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: GoStdlibMethods = new Map([['Error', '0/1']]); + +const parsed = new Map(); + +/** + * 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): 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 listed = Object.prototype.hasOwnProperty.call(GO_STDLIB_METHODS, importPath) + ? GO_STDLIB_METHODS[importPath]![typeName] + : undefined; + methods = typeof listed === 'string' + ? new Map(listed.split(' ').map((entry) => entry.split(':') as [string, string])) + : null; + parsed.set(key, methods); + } + return methods ?? undefined; +} diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index cb992fb7..215a06c2 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -2435,7 +2435,7 @@ export const GO_STDLIB_PACKAGES: ReadonlySet = new Set([ 'utilruntime', 'utilwait', 'utilnet', ]); -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. */ @@ -2452,7 +2452,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 {};