diff --git a/CHANGELOG.md b/CHANGELOG.md index 028a657ff..f65c9d0ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, a call through a variable that holds the result of a type assertion, like `st.Fetch(key)` after `st := v.(storage.Store)` or `rc.Read(p)` after `rc, ok := v.(ReadCloser)`, now links to that method of the asserted type, as a call made through the assertion itself does, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, such a call linked to whichever method shared the name or to nothing, and a variable named like a standard-library package, like grpc-go's `parser, ok := builder.(balancer.ConfigParser)`, was taken for that package. Re-index Go projects after upgrading. - In Go, a method called on an element of a slice, array or map, like kubectl's `schemas[ix].ValidateBytes(data)`, `pods[i].DeepCopy()`, `m.Items[len(m.Items)-1].Unmarshal(…)` in generated protobuf code or `(*in)[i].DeepCopyInto(&(*out)[i])` in every deepcopy file, now links to that method of the element's type. The type is read from what is indexed: a parameter, a variable, a struct field or a named slice type like `type ConjunctiveSchema []Schema`, and an element held by pointer, like `[]*Item`, reaches `Item`'s methods. Before, such a call linked to whichever method shared its name, often one in an unrelated package, or to nothing when many types declare it, as with Kubernetes' `DeepCopyInto`. An element whose type comes from outside your project, like `[]*net.IPNet`, or is a built-in one like `[]error`, now links to nothing rather than to a project method of that name. Re-index Go projects after upgrading. - In Go, a method called further along a chain now links to that method of the type the chain has reached, like `Where` and `First` in a GORM-generated query such as `query.Card.WithContext(ctx).Where(…).First()`, a method called on what a function returns, like the client from `store.New(…)`, or one called on a composite literal, like `GitOracle{}.Worktree()`. The type is read from a function's or method's results, a conversion, a literal or an embedded field. Before, such a call was matched by its method's name alone, so a query's `Where` linked to another table's query type, a literal's method to a type that only shares its name, and `time.Now().UTC().Format(…)` to the one project method named `Format`. A chain whose type comes from outside your project, or can't be worked out, now links to nothing rather than to a guess. Re-index Go projects after upgrading. Thanks @BX-D for the report and @inth3shadows for the extra cases. (#2543) +- In Go, a method called on a variable that holds what a call returns, like `r.Reset()` after `r := newRing()`, `v1.GET(…)` after `v1 := router.Group("/v1")`, `w.Stop()` after `w := NewWatcher()` from a dot-imported package, or the second value of `c, r := CreateTestContext(w)`, now links to that method of the type the call returns, and so does one called after a conversion, like `list.Usernames()` after `list := web.Users(names)`, or through a field that holds a function. A value of a type from outside your project, like `c, _ := net.Dial(…)`, an `*os.File` or an `http.Handler` a project function returns, now links to nothing instead of to a project method that only shares the name. Before, such calls were matched by the method's name alone, so etcd's `lg := s.Logger()` linked its `lg.Info(…)` calls to a project logger's `Info`, and prometheus's `NewWriteHandler(…)` results to the read handler's `ServeHTTP`. Re-index Go projects after upgrading. Thanks @danusha2345. (#1954) - In Go, three kinds of call now link where Go says they go. A conversion like `Category(s)`, `(*Pair)(p)` or `model.Kind(3)` is recorded as a use of the type rather than a call to it or a new instance of it. A call in a package's external tests (`package a_test`) reaches the helper those tests declare, not a same-named one in `package a` beside them, which they can't see. And a function, method or type a package defines once per platform or build tag, like `sep` in `sep_unix.go` and `sep_other.go`, now links to the definition a default build on Linux uses, or the one for the platform the calling file is written for, like Windows code reaching the Windows definition. Before, the second and third picked whichever definition came first, so a Linux caller could reach the Windows or `!unix` code. Re-index Go projects after upgrading. Thanks @inth3shadows for the report. (#2581) - In Go, a method a type declares in a file too large to index, like the `String`, `Marshal` and `Unmarshal` every Kubernetes API type has in `core/v1/generated.pb.go`, is no longer replaced by the method of that name a type it embeds has. Before, `pod.String()` on a `*v1.Pod` linked to `TypeMeta`'s `String`, a method the call never runs; it now links to nothing, since the pod's own method isn't in the graph. A method the type really gets from a type it embeds, like `GetName` from `ObjectMeta`, still links. Re-index Go projects after upgrading. - In Go, calls into a module whose `go.mod` is not at the project root now resolve, whether it is a `server/` backend next to a `web/` frontend or one of several modules side by side as in etcd, so calls like `store.New()` and `s.db.CreateItem()` find their targets instead of being treated as calls into a third-party package. A name written through a package, like a `job.OPCommand` result type or a field of type `artifact.Manager`, now links to that package's symbol rather than a same-named one elsewhere, which also corrects links in projects with a single `go.mod`. Re-index Go projects after upgrading. Thanks @GoDiao for the report and @danusha2345 for the fix. (#2322) diff --git a/__tests__/go-chained-receivers.test.ts b/__tests__/go-chained-receivers.test.ts index 9f097df0a..ca83ac4c4 100644 --- a/__tests__/go-chained-receivers.test.ts +++ b/__tests__/go-chained-receivers.test.ts @@ -364,7 +364,8 @@ func useLiteral() { func main() { use(holder{}); useLiteral() } `, }); - expect(callees(graph, 'use')).toEqual([]); + // A func-typed field's call gives the result its type declares: `h.get()` is an A. + expect(callees(graph, 'use')).toEqual(['calls method A::Run']); expect(callees(graph, 'useLiteral')).toEqual([]); }); }); diff --git a/__tests__/go-receiver-type.test.ts b/__tests__/go-receiver-type.test.ts new file mode 100644 index 000000000..1d0c7e8dd --- /dev/null +++ b/__tests__/go-receiver-type.test.ts @@ -0,0 +1,454 @@ +/** + * Go method calls through a receiver whose type is written somewhere other + * than a plain parameter of the caller's own package: + * + * - a package-qualified parameter (`s *store.Store`); + * - the result of a function or a conversion (`r := newRing()`, `s := + * store.NewStore()`, `list := web.Users(names)`). + * + * A receiver typed outside the project (`conn net.Conn`, `ctx + * context.Context`, `c, _ := net.Dial(…)`, an alias `type Ctx = + * context.Context`) gets no edge rather than a project method that happens + * to share the name, and so does a project function's result declared as an + * outside type (`*os.File`, `http.RoundTripper`). + * + * Cases first written for PR #1954 by @danusha2345. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { CodeGraph } from '../src'; + +async function indexProject(files: Record): Promise<{ dir: string; cg: CodeGraph }> { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-go-recv-')); + fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/app\n\ngo 1.22\n'); + for (const [file, source] of Object.entries(files)) { + fs.mkdirSync(path.join(dir, path.dirname(file)), { recursive: true }); + fs.writeFileSync(path.join(dir, file), source); + } + const cg = CodeGraph.initSync(dir); + await cg.indexAll(); + return { dir, cg }; +} + +/** `file::Qualified::name` of every method `caller` (a function name) calls. */ +async function methodCallees(cg: CodeGraph, caller: string): Promise { + const node = (await cg.searchNodes(caller, { limit: 20 })).find( + (r) => r.node.name === caller && r.node.kind === 'function', + ); + expect(node, caller).toBeDefined(); + return (await cg.getCallees(node!.node.id)) + .filter((c) => c.node.kind === 'method') + .map((c) => `${c.node.filePath}::${c.node.qualifiedName}`) + .sort(); +} + +describe('Go receiver typing', () => { + let dir: string; + let cg: CodeGraph; + + beforeAll(async () => { + ({ dir, cg } = await indexProject({ + 'main.go': `package main + +import ( + "bytes" + "net" + "net/http" + "os" + + "example.com/app/other" + "example.com/app/store" +) + +type ringLog struct{ buf []byte } + +func (r *ringLog) Write(b []byte) {} +func (r *ringLog) Reset() {} +func (r *ringLog) Reader() *reader { return &reader{} } + +type reader struct{} + +func (r *reader) Reset() {} + +type Engine struct{} + +func (e *Engine) Run() *Engine { return e } +func (e *Engine) Stop() {} + +func newRing() *ringLog { return &ringLog{} } +func NewEngine(n int) (*Engine, error) { return &Engine{}, nil } +func open() (*os.File, error) { return nil, nil } +func wrap(e *Engine) *ringLog { return nil } + +type limiter struct{} + +func (l *limiter) RoundTrip(r *http.Request) (*http.Response, error) { return nil, nil } + +func transport() http.RoundTripper { return &limiter{} } + +func lowerParam(ring *ringLog, b []byte) { ring.Write(b) } +func lowerValueParam(ring ringLog) { ring.Reset() } +func groupedParam(a, ring *ringLog) { ring.Reset() } +func lowerCtor() { r := newRing(); r.Reset() } +func exportedCtor() { e, _ := NewEngine(1); e.Stop() } +func qualifiedParam(s *store.Store) { s.Put("a") } +func qualifiedCtor() { s := store.NewStore(); s.Put("b") } + +func multiLineCtor() { + e, err := NewEngine( + 1, + ) + _ = err + e.Stop() +} + +func ifCtor() { + if r := newRing(); r != nil { + r.Write(nil) + } +} + +// \`r\` is what Reader returns: a reader, not a ringLog. +func chainedCtor() { r := newRing().Reader(); r.Reset() } + +// On the binding's own line \`e\` is still the parameter; inside the +// \`if\`, \`e\` is what wrap returns. +func selfArg(e *Engine) { + if e := wrap(e.Run()); e != nil { + e.Write(nil) + } +} + +func stdlibParam(buf *bytes.Buffer) { buf.Truncate(0) } +func stdlibCtor() { c, _ := net.Dial("tcp", ""); c.RemoteAddr() } +func projectFuncReturningStdlib() { f, _ := open(); f.Truncate(0) } +func interfaceResult() { rt := transport(); rt.RoundTrip(nil) } +func netParam(conn net.Conn) { conn.LocalAddr() } + +// The \`r\` of the block, or of the function literal, is gone by the call. +func blockShadow() { + r := newRing() + if true { + r := other.New() + _ = r + } + r.Write(nil) +} + +func closureShadow() { + r := newRing() + go func() { r := other.New(); _ = r }() + r.Write(nil) +} + +type Holder struct{} + +func (h *Holder) New() *Item { return &Item{} } + +type Item struct{} + +func (i *Item) Save() {} + +// \`store\` is the parameter, not the imported package. +func shadowPkg(store *Holder) { x := store.New(); x.Save() } + +func main() {} +`, + 'store/store.go': `package store + +type Store struct{} + +func NewStore() *Store { return &Store{} } + +func (s *Store) Put(k string) {} + +type Thing struct{} + +func New() *Thing { return &Thing{} } + +func (t *Thing) Save() {} +`, + 'other/other.go': `package other + +type Other struct{} + +func New() *Other { return &Other{} } + +func (o *Other) Write(b []byte) {} +`, + // Same-named methods elsewhere, so a guess by name has somewhere wrong to go. + 'decoy/decoy.go': `package decoy + +type Decoy struct{} + +func (d *Decoy) Write(b []byte) {} +func (d *Decoy) Reset() {} +func (d *Decoy) Run() {} +func (d *Decoy) Stop() {} +func (d *Decoy) Put(k string) {} +func (d *Decoy) Truncate(n int) {} +func (d *Decoy) RemoteAddr() {} +func (d *Decoy) LocalAddr() {} + +type Store struct{} + +func (s *Store) Put(k string) {} +`, + })); + }); + + afterAll(() => { + cg?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + }); + + it.each([ + ['lowerParam', ['main.go::ringLog::Write']], + ['lowerValueParam', ['main.go::ringLog::Reset']], + ['groupedParam', ['main.go::ringLog::Reset']], + ])('%s: a receiver named like a standard-library package resolves on its type', async (caller, expected) => { + expect(await methodCallees(cg, caller)).toEqual(expected); + }); + + it.each([ + ['lowerCtor', ['main.go::ringLog::Reset']], + ['exportedCtor', ['main.go::Engine::Stop']], + ['multiLineCtor', ['main.go::Engine::Stop']], + ['ifCtor', ['main.go::ringLog::Write']], + ['qualifiedCtor', ['store/store.go::Store::Put']], + ])('%s: a receiver bound to a call resolves on what the callee returns', async (caller, expected) => { + expect(await methodCallees(cg, caller)).toEqual(expected); + }); + + it('a package-qualified parameter resolves in that package', async () => { + expect(await methodCallees(cg, 'qualifiedParam')).toEqual(['store/store.go::Store::Put']); + }); + + it('a binding is typed by the whole chain, and from the end of its own line', async () => { + expect(await methodCallees(cg, 'chainedCtor')).toEqual(['main.go::reader::Reset', 'main.go::ringLog::Reader']); + expect(await methodCallees(cg, 'selfArg')).toEqual(['main.go::Engine::Run', 'main.go::ringLog::Write']); + }); + + it.each(['stdlibParam', 'stdlibCtor', 'projectFuncReturningStdlib', 'netParam'])( + '%s: a receiver typed outside the project gets no edge', + async (caller) => { + expect(await methodCallees(cg, caller)).toEqual([]); + }, + ); + + // Real code's by-name guesses here mostly took a namesake: prometheus's + // `handler := NewWriteHandler(…)` went to `readHandler::ServeHTTP`. + it('an outside interface a project function returns links nothing', async () => { + expect(await methodCallees(cg, 'interfaceResult')).toEqual([]); + }); + + it.each(['blockShadow', 'closureShadow'])( + '%s: a `:=` in a block that closed before the call is another variable', + async (caller) => { + expect(await methodCallees(cg, caller)).toEqual(['main.go::ringLog::Write']); + }, + ); + + it('a parameter named like an imported package is the parameter, and types what its method returns', async () => { + expect(await methodCallees(cg, 'shadowPkg')).toEqual(['main.go::Holder::New', 'main.go::Item::Save']); + }); +}); + +describe('Go receiver typing through conversions and aliases', () => { + let dir: string; + let cg: CodeGraph; + + beforeAll(async () => { + ({ dir, cg } = await indexProject({ + 'web/context.go': `package web + +type Context struct{} + +func (c *Context) MakeAuditRecord() {} + +type Users []string + +func (u Users) Usernames() []string { return nil } +`, + 'api4/handlers.go': `package api4 + +import ( + "context" + + "example.com/app/web" +) + +type Context = web.Context + +type ( + Handler = *web.Context + Ctx = context.Context +) + +type Defined web.Context + +func projectAlias(h Handler) { h.MakeAuditRecord() } +func conversion(names []string) { list := web.Users(names); list.Usernames() } +func definedType(d *Defined) { d.MakeAuditRecord() } +func stdlibReceiver(ctx context.Context) { ctx.Done() } +func aliasedStdlib(ctx Ctx) { ctx.Done() } +`, + // Same-named types and methods elsewhere, so neither a lookup by type + // name nor a guess by method name has a single answer to fall back on. + 'decoy/decoy.go': `package decoy + +type Context struct{} + +func (c *Context) MakeAuditRecord() {} + +// Named like the alias of an outside type in api4. +type Ctx struct{} + +func (c *Ctx) Done() {} + +type Decoy struct{} + +func (d *Decoy) Done() {} +func (d *Decoy) Usernames() {} +`, + })); + }); + + afterAll(() => { + cg?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + }); + + it('a receiver bound to a conversion resolves on the converted type', async () => { + expect(await methodCallees(cg, 'conversion')).toEqual(['web/context.go::Users::Usernames']); + }); + + it('an alias of a project type still resolves on that type', async () => { + expect(await methodCallees(cg, 'projectAlias')).toEqual(['web/context.go::Context::MakeAuditRecord']); + }); + + it.each(['definedType', 'stdlibReceiver', 'aliasedStdlib'])( + '%s: a defined type, or a receiver typed outside the project, gets no edge', + async (caller) => { + expect(await methodCallees(cg, caller)).toEqual([]); + }, + ); +}); + +describe('Go receiver typing through what a call hands back', () => { + let dir: string; + let cg: CodeGraph; + + beforeAll(async () => { + ({ dir, cg } = await indexProject({ + 'svc/svc.go': `package svc + +import ( + "sync" + + "google.golang.org/grpc" +) + +type Client struct{} + +func (c *Client) Do() {} + +type monitor struct { + client func() (*Client, error) +} + +func (m *monitor) run() { + c, _ := m.client() + c.Do() +} + +type Result struct{} + +func (r *Result) Done() {} + +type Converter struct{} + +func (c *Converter) Convert() (*Result, error) { return &Result{}, nil } + +var pool sync.Pool + +func pooled() { + conv := pool.Get().(*Converter) + out, _ := conv.Convert() + out.Done() +} + +type Resp struct{} + +func (r *Resp) GetKind() string { return "" } + +func stream(s grpc.ServerStreamingClient[Resp]) { + chunk, _ := s.Recv() + chunk.GetKind() +} +`, + 'watch/watch.go': `package watch + +type Watcher struct{} + +func NewWatcher() *Watcher { return &Watcher{} } + +func (w *Watcher) Stop() {} +`, + 'watch/watch_test.go': `package watch_test + +import ( + "testing" + + . "example.com/app/watch" +) + +func TestStop(t *testing.T) { + w := NewWatcher() + w.Stop() +} +`, + // Namesakes, so a guess by the method's name has somewhere wrong to go. + 'decoy/decoy.go': `package decoy + +type Decoy struct{} + +func (d *Decoy) Do() {} +func (d *Decoy) Done() {} +func (d *Decoy) Stop() {} +`, + })); + }); + + afterAll(() => { + cg?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + }); + + const callees = async (caller: string, kind: 'function' | 'method' = 'function'): Promise => { + const node = (await cg.searchNodes(caller, { limit: 20 })).find((r) => r.node.name === caller && r.node.kind === kind); + expect(node, caller).toBeDefined(); + return (await cg.getCallees(node!.node.id)) + .filter((c) => c.node.kind === 'method') + .map((c) => `${c.node.filePath}::${c.node.qualifiedName}`) + .sort(); + }; + + it('a func-typed field is called for the result its type declares', async () => { + expect(await callees('run', 'method')).toEqual(['svc/svc.go::Client::Do']); + }); + + it('an assertion types the value, whatever hands it out', async () => { + expect(await callees('pooled')).toEqual(['svc/svc.go::Converter::Convert', 'svc/svc.go::Result::Done']); + }); + + it('a function a dot import brings in types its result', async () => { + expect(await callees('TestStop')).toEqual(['watch/watch.go::Watcher::Stop']); + }); + + it('what an outside generic type hands back is left to name matching, as before', async () => { + expect(await callees('stream')).toEqual(['svc/svc.go::Resp::GetKind']); + }); +}); diff --git a/__tests__/go-std-method-receivers.test.ts b/__tests__/go-std-method-receivers.test.ts index 9866345a2..9936d6406 100644 --- a/__tests__/go-std-method-receivers.test.ts +++ b/__tests__/go-std-method-receivers.test.ts @@ -310,19 +310,22 @@ describe('Go standard method names on a receiver nothing types', () => { it.each([ // `for _, id := range ids` ['idsText', 'types/id.go::ID::String instance-method@0.65'], - // `urlsMap := types.NewURLsMap()` - ['urls', 'types/id.go::URLsMap::String instance-method@0.65'], ])('still guesses for %s through a receiver named after the owner type', (fn, target) => { expect(calls(fn)).toEqual([target]); }); + // `urlsMap := types.NewURLsMap()`: the type the package's function returns. + it('links urls through the type the call it is assigned from returns', () => { + expect(calls('urls')).toEqual(['types/id.go::URLsMap::String instance-method@0.9']); + }); + it.each([ // `var ( query promql.Query )`, read past the table's `query string`; // `Exec`, no standard name, is typed by that declaration too. ['runQueries', ['promql/engine.go::Query::Exec instance-method@0.9', 'promql/engine.go::Query::String instance-method@0.9']], - // `version, _ := ParseVersion(…)` says no type, and the table's - // `version string` is another declaration: nothing is ruled out. - ['versionText', ['server/version.go::Version::Major instance-method@0.8', 'server/version.go::Version::String instance-method@0.8']], + // `version, _ := ParseVersion(…)` is what ParseVersion returns; the + // table's `version string` is another declaration. + ['versionText', ['server/version.go::Version::Major instance-method@0.9', 'server/version.go::Version::String instance-method@0.9']], ])('reads %s by the declaration in scope, not a test table field of the name', (fn, targets) => { expect(calls(fn)).toEqual(targets); }); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 224c4b118..75ecfe695 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -320,9 +320,11 @@ function matchMemberFunctionRef(ref: UnresolvedRef, context: ResolutionContext): // One declared as a type literal, a type from outside the project or an // alias of one holds no project method to take. if (decl.declared === null || (decl.declared !== undefined && goHasNoProjectMethods(decl.declared, ref, context))) return null; + // Nor one a call or a conversion gives a value of such a type. + if (decl.valued === null || (decl.valued && goValuedHasNoProjectMethods(decl.valued, context))) return null; if (type) { return resolveMethodOnType(type, member, ref, context, 0.9, 'function-ref', - goDeclaredTypePackage(decl.raw, ref.filePath, context)); + goDeclaredTypePackage(decl.raw, decl.file ?? ref.filePath, context)); } const types = context.getNodesByName(receiver).filter(n => n.language === 'go' && (n.kind === 'struct' || n.kind === 'interface')); if (types.length) return types.length === 1 ? resolveMethodOnType(receiver, member, ref, context, 0.9, 'function-ref') : null; @@ -19319,7 +19321,8 @@ export function inferLocalReceiverType( // *v1.Pod`), which the patterns below don't read, each name of `a, b // rest.Interface`, a `var (…)` spec. The patterns scan back for the name // with no block scope, so they can also read another declaration of it. - const type = /^[A-Za-z_]\w*$/.test(scanReceiver) ? goLocalDecl(scanReceiver, ref, context)?.type : undefined; + const local = /^[A-Za-z_]\w*$/.test(scanReceiver) ? goLocalDecl(scanReceiver, ref, context) : undefined; + const type = local?.type; if (type !== undefined) { const named = goNamedType(type); decl.declared = named; @@ -19327,6 +19330,31 @@ export function inferLocalReceiverType( decl.raw = named; return named.slice(named.indexOf('.') + 1); } + // A local its declaration in scope gives the result of a call or a + // conversion — `r := newRing()`, `e, err := NewEngine(1)`, `v1 := + // router.Group(…)`, `list := web.Users(names)` — has the type of that + // value, read as the indexed and chained calls read it (#2782): what the + // function or method returns (the k-th result of several), or the type + // converted to. The patterns below read none of these. A value of a type + // from outside the project has no project method, whether an outside + // package hands it out (`c, _ := net.Dial(…)`) or a project function + // returns it (`*os.File`, `http.RoundTripper`, a `reflect.Value`): a + // guess by the method's name took a namesake's (#1954). + const index = local?.value ? goScopeIndex(ref.filePath, context) : null; + if (local?.value && index && goValueIsCall(local, index)) { + const valued = goDeclaredValueType(local, ref.filePath, index, context, 0); + if (valued !== undefined) { + const named = valued && goNamedType(valued.text); + if (!named) { + decl.valued = null; + return null; + } + decl.valued = valued; + decl.raw = named; + decl.file = valued.file; + return named.slice(named.indexOf('.') + 1); + } + } } const escapedReceiver = scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -20337,6 +20365,8 @@ export function matchMethodCall( // `query string`. Only a type read there rules a call out below. A slice, // map, channel, function or interface literal type has no project method. if (decl.declared === null) return null; + // So does a local a call gives a value of such a type (`c, _ := net.Dial(…)`). + if (decl.valued === null) return null; const goDeclared = decl.declared; // A Go standard method name says nothing of its receiver, so its // declaration decides: etcd's parameter `id types.ID` makes `id.String()` @@ -20436,6 +20466,9 @@ export function matchMethodCall( // `c.Now()` on a `clock.PassiveClock` to apimachinery's `Clock`, and gin // declares a `ResponseWriter` of its own. if (goDeclared !== undefined && goHasNoProjectMethods(goDeclared, ref, context)) return null; + // So does a local whose value's type, read in the file that writes it + // (a callee's result), is one from outside the project. + if (decl.valued && goValuedHasNoProjectMethods(decl.valued, context)) return null; // Java/Kotlin: when two classes share the simple name, the file's import // pins WHICH one (#314); Go: the package that declares the type (#2323); // TS/JS: the file declaring the class the file imports under that name. @@ -20446,7 +20479,7 @@ export function matchMethodCall( .getImportMappings(ref.filePath, ref.language) .find((i) => i.localName === inferredType)?.source : ref.language === 'go' - ? goDeclaredTypePackage(decl.raw, ref.filePath, context) + ? goDeclaredTypePackage(decl.raw, decl.file ?? ref.filePath, context) : ESM_FAMILY.has(ref.language) && !awaited ? nmTimedT('mc-tsimport', ref, () => tsImportedTypeFile(decl.raw ?? inferredType!, ref, context)) : undefined; @@ -20518,7 +20551,7 @@ export function matchMethodCall( // strategies below, as one on a receiver of unread type does: a type // defined over an interface (`type Codec Serializer`) has methods the // index doesn't give it. - if (goStd && goDeclared !== undefined) return null; + if (goStd && (goDeclared !== undefined || decl.valued)) return null; // Nor one the type declares in a file the index couldn't read: in // kubernetes, `pod.Unmarshal(…)` on a `*v1.Pod` is the pod's own, in // core/v1's generated.pb.go, never the `TypeMeta` one a name finds. @@ -21606,6 +21639,10 @@ interface GoProjectType { */ interface GoReceiverDecl { raw?: string; + /** The file `raw` is written in, when it is not the call's: a callee's signature spells a result's type. */ + file?: string; + /** The type of the call's or conversion's value a Go local's declaration gives it; null for one with no project method. */ + valued?: GoTypeText | null; asserted?: GoProjectType | null; declared?: string | null; /** The line the inference read the declaration on. */ @@ -22466,6 +22503,41 @@ function goMethodOnElement(t: GoTypeText, ref: UnresolvedRef, context: Resolutio return resolveMethodOnType(named.name, ref.referenceName, ref, context, 0.9, 'instance-method', named.dir); } +/** + * Whether the value a Go declaration gives its name is a call or a + * conversion: `f(…)`, `pkg.F(…)`, `x.M(…)`, `T(x)`, `a.b().C(…)`, alone or + * as this name's place among several values (`a, b := f(), g()`). Not a + * `range` clause, a builtin's `make`/`new`/`append`, an assertion + * (goAssertedLocalType reads those) or a type switch's guard. + */ +function goValueIsCall(local: GoDecl, index: GoScopeIndex): boolean { + const { at, k, n } = local.value!; + const code = index.code; + const rhs = code.slice(at, local.kind === 'header' ? goHeaderValueEnd(code, at) : goStatementEnd(code, at)); + if (/^\s*range\b/.test(rhs)) return false; + const values = goSplitTopLevel(rhs, ','); + const value = values.length === n ? values[k]! : rhs; + // A type switch's guard (`switch v := x.(type)`) has each case's type, and + // an assertion (`v := x.(T)`) is goAssertedLocalType's to read. + if (/\.\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*$/.test(value)) return false; + return /^\s*[A-Za-z_(][\s\S]*\)\s*$/.test(value) && !/^\s*(?:new|make|append)\s*\(/.test(value); +} + +/** + * Whether a Go value of type t has no method the project declares: + * a pointer to a pointer, a type literal, a type from outside the project or + * a predeclared one, or an alias of such a type. A type parameter or a type + * the index can't tell is not ruled out. + */ +function goValuedHasNoProjectMethods(t: GoTypeText, context: ResolutionContext): boolean { + const text = t.text.replace(/^\*/, ''); + if (text.startsWith('*') || GO_TYPE_LITERAL.test(text)) return true; + const named = goNamedTypeDecls({ ...t, text }, context); + if (named === null) return true; + if (named === undefined) return false; + return named.nodes.every((n) => goAliasTarget(n, context) === null); +} + /** * Whether a file of the Go package in `dir` that the index couldn't read * declares `typeName`'s method `methodName` (or can't be read either): the @@ -22588,6 +22660,13 @@ function goReceiverChainType( ): GoTyped { const head = links[0]; if (!head || head.kind !== 'operand' || depth > GO_VALUE_DEPTH) return undefined; + // `….(T)` is a T, whatever the expression before it: prometheus's + // `rw.converterPool.Get().(*otlptranslator.PrometheusConverter)` asserts + // what an outside `sync.Pool` hands back. + const last = links[links.length - 1]; + if (result === 0 && links.length > 1 && last?.kind === 'assert' && last.text !== 'type') { + return goTypeTextAt(last.text, filePath, at, index, context); + } // The result a call link gives: the one asked for when it ends the chain. const resultOf = (k: number): number => (k === links.length - 1 ? result : 0); let t: GoTyped; @@ -22685,6 +22764,16 @@ function goCalledType( const fns = context.getNodesByName(name).filter((n) => n.language === 'go' && n.kind === 'function' && goPackageDir(n.filePath) === dir && (imported ? !testFile(n.filePath) : goPackageClause(n.filePath, context) === clause && (testFile(fromFile) || !testFile(n.filePath)))); + // A bare call the file's own package doesn't declare may be a package's + // the file dot-imports: kubernetes' `package watch_test` calls + // `NewStreamWatcher(…)` through `. "k8s.io/apimachinery/pkg/watch"`, the + // package of its own directory. + if (fns.length === 0 && !imported && goPackageTypes(name, dir, context).length === 0) { + for (const d of goDotImports(fromFile, context).dirs) { + const t = goCalledType(name, d, written, fromFile, true, at, index, context, depth + 1, result); + if (t !== undefined) return t; + } + } if (fns.length === 0) { // `T(x)`: a conversion, when the package declares the type. return result === 0 && goPackageTypes(name, dir, context).length > 0 ? goTypeTextAt(written, fromFile, at, index, context) : undefined; @@ -22700,10 +22789,15 @@ function goCalledType( /** * The type a Go method call `x.name(…)` gives on an `x` of type t: the first - * result of the method Go selects (goMethodOnElement). Null when the method - * is no project type's (t comes from outside the project, or a project type - * gets it from an embedded outside one): what it returns is unknown to the - * project too. + * result of the method Go selects (goMethodOnElement), or of the function a + * func-typed field holds (harbor's `w.jobServiceRedisClient()` through a + * `jobServiceRedisClient func() (jm.RedisClient, error)` field). Null when t + * comes from outside the project or is predeclared: what its method returns + * is no type of the project's. Undefined when that can't be told: a struct or + * interface literal (`tc.load()` on a test table's `struct{ load func() Q }`), + * a generic type of a package from outside the project, whose method can hand + * back its type argument (grpc's `ServerStreamingClient[RangeStreamResponse]`'s + * `Recv`), or a project type whose method the index doesn't show. */ function goMethodResultType(t: GoTypeText, name: string, filePath: string, context: ResolutionContext, depth: number, result = 0): GoTyped { if (depth > GO_VALUE_DEPTH) return undefined; @@ -22711,9 +22805,27 @@ function goMethodResultType(t: GoTypeText, name: string, filePath: string, conte fromNodeId: '', referenceName: name, referenceKind: 'calls', line: 1, column: 0, filePath, language: 'go', }; const method = goMethodOnElement(t, probe, context); - if (!method) return method; - const node = context.getNodeById?.(method.targetNodeId); - return node ? goSignatureResultType(node, context, result) : undefined; + if (method) { + const node = context.getNodeById?.(method.targetNodeId); + return node ? goSignatureResultType(node, context, result) : undefined; + } + const viaField = goFuncFieldResultType(t, name, context, depth + 1, result); + if (viaField !== undefined || method === undefined) return viaField; + const text = t.text.replace(/^\*/, ''); + if (/^(?:struct|interface)\b/.test(text) || (!GO_TYPE_LITERAL.test(text) && /\]$/.test(text))) return undefined; + return goNamedTypeDecls({ ...t, text }, context) ? undefined : null; +} + +/** + * What calling a func-typed field `name` of a value of type t gives: result + * `result` of the func type the struct declares the field with. Undefined + * for no such field, or one of another type. + */ +function goFuncFieldResultType(t: GoTypeText, name: string, context: ResolutionContext, depth: number, result = 0): GoTyped { + const field = goFieldTypeOf(t, name, context, depth + 1); + if (!field || !/^func\s*\(/.test(field.text)) return undefined; + const text = goResultText(field.text.replace(/^func\s*/, ''), result); + return text === undefined ? undefined : { text: goNormalizeType(text), file: field.file, params: field.params }; } /** @@ -22726,7 +22838,20 @@ function goMethodResultType(t: GoTypeText, name: string, filePath: string, conte * `Set[T]` is a `Set`. */ function goSignatureResultType(fn: Node, context: ResolutionContext, result = 0): GoTypeText | undefined { - const sig = fn.signature?.trim(); + const text = goResultText(fn.signature ?? '', result); + if (text === undefined) return undefined; + const index = goScopeIndex(fn.filePath, context); + const params = index ? goTypeParamsAt(index, fn.filePath, index.lineStarts[fn.startLine - 1] ?? 0, context) : undefined; + return { text: goNormalizeType(text), file: fn.filePath, params }; +} + +/** + * Result `result` of a Go signature written `(params) results`, as written: + * `CardDo` of `(value int) CardDo`, `*Response` of `(ctx context.Context) + * (*Response, error)`, `int` of `() (n int, err error)`. Undefined for none. + */ +function goResultText(signature: string, result = 0): string | undefined { + const sig = signature.trim(); if (!sig || sig[0] !== '(') return undefined; const close = goClosingBracket(sig, 0); if (close < 0) return undefined; @@ -22749,9 +22874,7 @@ function goSignatureResultType(fn: Node, context: ResolutionContext, result = 0) if (!wanted) return undefined; text = wanted.trim(); } else if (result > 0) return undefined; - const index = goScopeIndex(fn.filePath, context); - const params = index ? goTypeParamsAt(index, fn.filePath, index.lineStarts[fn.startLine - 1] ?? 0, context) : undefined; - return { text: goNormalizeType(text), file: fn.filePath, params }; + return text; } /** A type written at offset `at` of a file, or at package level for no `index`. */ @@ -24621,6 +24744,8 @@ function matchGoFieldChainCall( const asserted = decl.asserted; if (asserted === null) return null; if (decl.declared && goHasNoProjectMethods(decl.declared, ref, context)) return null; + // Nor one a call gives a value of a type from outside the project. + if (decl.valued && goValuedHasNoProjectMethods(decl.valued, context)) return null; const fieldEsc = field!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`); @@ -24633,7 +24758,7 @@ function matchGoFieldChainCall( // names repeat across packages — harbor has a `daoTestSuite` in every DAO // package — and another package's same-named struct with a same-named // field is not this one (#2323). - const basePkg = asserted?.dir ?? goDeclaredTypePackage(decl.raw, ref.filePath, context); + const basePkg = asserted?.dir ?? goDeclaredTypePackage(decl.raw, decl.file ?? ref.filePath, context); const declared = structs.filter((n) => goPackageDir(n.filePath) === basePkg); if (declared.length > 0 || asserted) structs = declared; structs = preferCallSiteFile(structs, ref.filePath);