From 76d06f0b88b5a119ee3d6e82df96ea4f6d5fbeed Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Wed, 7 Oct 2026 14:11:50 +0300 Subject: [PATCH] fix(go): type receivers from qualified parameters and call results; no guess for outside types Two receiver shapes still got no type, and a receiver typed outside the project still got a guess by method name: - A parameter or receiver of a package-qualified type (`s *store.Store`). - A receiver bound to a call: `r := newRing()`, `s, err := store.NewStore()`, `if r := find(id); r != nil`, a call spread over several lines, a conversion (`list := model.BotList(bots)`). The type is the callee's first result as its signature spells it, read in the callee's file. Only when the call is the whole right-hand side, and not when it takes the receiver itself. A receiver whose declared type comes from a package outside the project's modules (`conn net.Conn`, `ctx context.Context`, `req *http.Request`, an alias of one, the result of an outside function) gets no edge: `ctx.Done()` went to the one project type declaring a `Done`. A project function's result declared as an outside type by value (`http.RoundTripper`) is left as it was, since that is usually an interface a project type implements. Aliases of project types are followed by #2417, which gave them nodes; the alias reader this change carried is gone, and an alias of an outside type is recognized from that node's declaration. A variable named like a standard-library package (`ring`, `token`, `parser`) is #2489's now: this change carried its own rule for it (skip only when the file imports the package), which is dropped in favour of upstream's `isGoProjectLocalCall`, and the standard-library package check is upstream's again, untouched. Resolver only: no extraction change, the receiver scan keeps its per-line memo. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/go-receiver-type.test.ts | 281 +++++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 186 ++++++++++++++++--- 3 files changed, 440 insertions(+), 28 deletions(-) create mode 100644 __tests__/go-receiver-type.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eb9e73572..2a9ff4bba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In React with TypeScript, a component that renders a generic component and passes it type arguments, like outline's ` items={…} />` or excalidraw's ` …>`, now links to the component it renders. Before, a tag followed by type arguments was skipped, so the generic component looked unused by the screens that render it, and `codegraph_explore`'s call paths, callers and impact stopped short of it. Re-index React projects after upgrading. - In React, a component that a file declares itself as a value is now what that file's JSX renders, whatever else in the repository shares its name. A lazily loaded page, like `const RegisterPage = Loadable(lazy(() => import('./pages/Register')))`, a `dynamic(() => import(…))` component or one loaded only in development (`IS_DEV ? lazy(…) : null`), links to the component its module exports, including the one its loader picks and one an `export default observer(Login)` wraps; a wrapped component like `const Settings = observer(function Settings() {…})`, `React.memo(Rows, areEqual)` or `forwardRef(ComboboxInner) as {…}` links to the function it wraps; and an alias like `const DialogPortal = DialogPrimitive.Portal` links to its own declaration. Before, `` was linked to any lone component of that name elsewhere, often in another app of the same repository, so `codegraph_explore`, callers and impact followed it into the wrong app while the page it really renders had no caller. React Router layouts declared this way follow the same rule, and a TSX type named like a value the file declares now stays in that file. Re-index React projects after upgrading. - In React, a JSX tag imported from one of your own modules now links to the component that module exports under that name, read the way a lazily loaded page's module already is. That includes going through an `index` file or barrel that passes the component on, like outline's `Sidebar/index.ts` with `import Sidebar from './App'; export default Sidebar` or its `Avatar/index.ts` with `const Avatar = AvatarWithHoverCard`, and past a wrapper such as `export default observer(AppSidebar)`, `React.memo(StaticCanvas, areEqual)` or mastodon's `const MemoizedBlurhash = memo(Blurhash)`. Before, a tag was matched by its name and the import only broke ties, so outline's `` linked to an unrelated styled `Sidebar` in a sibling file, a default import under another name linked to whatever other component its module exported, and an import through a barrel could land on another app's component of the same name. A tag whose module exports an unnamed component, like `export default () => …`, now links to nothing instead of to a namesake elsewhere. A lazily loaded React Router page exported as such a value or wrapper links to the component it holds too, the one with the markup. `codegraph sync` and the file watcher redraw these links when only the file that passes the component on changes. Re-index React projects after upgrading. +- In Go, more method calls now reach the right method: through a parameter whose type comes from another package (`s *store.Store`), and through the result of a function or a conversion (`r := newRing()`, `s := store.NewStore()`). A call on a value whose type comes from outside your project, like `ctx.Done()` on a `context.Context` or `req.WithContext(…)` on an `*http.Request`, or on an alias of such a type, no longer links to an unrelated project method that happens to share the name. Re-index Go projects after upgrading. (#1954) ## [1.6.2] - 2026-10-03 diff --git a/__tests__/go-receiver-type.test.ts b/__tests__/go-receiver-type.test.ts new file mode 100644 index 0000000000..8d50302f71 --- /dev/null +++ b/__tests__/go-receiver-type.test.ts @@ -0,0 +1,281 @@ +/** + * 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. A project function's result + * declared as an outside type by value (`http.RoundTripper`) is left as it + * was: that is usually an interface a project type implements. + */ +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/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, which is not read: not a ringLog. +func chainedCtor() { r := newRing().Reader(); r.Reset() } + +// On the binding's own line \`e\` is still the parameter. +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() } + +func main() {} +`, + 'store/store.go': `package store + +type Store struct{} + +func NewStore() *Store { return &Store{} } + +func (s *Store) Put(k string) {} +`, + // 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 not typed by the head of a call chain, nor on its own line', async () => { + expect(await methodCallees(cg, 'chainedCtor')).not.toContain('main.go::ringLog::Reset'); + expect(await methodCallees(cg, 'selfArg')).toEqual(['main.go::Engine::Run']); + }); + + it.each(['stdlibParam', 'stdlibCtor', 'projectFuncReturningStdlib', 'netParam'])( + '%s: a receiver typed outside the project gets no edge', + async (caller) => { + expect(await methodCallees(cg, caller)).toEqual([]); + }, + ); + + it('an outside interface a project function returns still reaches its implementation', async () => { + expect(await methodCallees(cg, 'interfaceResult')).toEqual(['main.go::limiter::RoundTrip']); + }); +}); + +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([]); + }, + ); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 2726405ffa..306fc9c2ac 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -313,8 +313,10 @@ function matchMemberFunctionRef(ref: UnresolvedRef, context: ResolutionContext): return decl.asserted && resolveMethodOnType(decl.asserted.name, member, ref, context, 0.9, 'function-ref', decl.asserted.dir); } if (type) { + const declFile = decl.file ?? ref.filePath; + if (isGoExternalType(decl.raw, declFile, context)) return null; return resolveMethodOnType(type, member, ref, context, 0.9, 'function-ref', - goDeclaredTypePackage(decl.raw, ref.filePath, context)); + goDeclaredTypePackage(decl.raw, declFile, 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; @@ -2273,9 +2275,13 @@ export function goTypePositionTarget(result: ResolvedRef, ref: UnresolvedRef, co function isGoExternalQualified(ref: UnresolvedRef, context: ResolutionContext): boolean { const { imported, typePackage } = goRefQualification(ref, context); const pkg = imported ?? typePackage; - if (!pkg) return false; - return !(pkg.source.startsWith('.') || pkg.source.includes('/internal/') || - context.getGoPackageDir?.(pkg.source, ref.filePath) != null); + return !!pkg && isGoExternalImport(pkg, ref.filePath, context); +} + +/** Whether a Go file's import names a package from outside the project's modules. */ +function isGoExternalImport(imported: ImportMapping, filePath: string, context: ResolutionContext): boolean { + return !(imported.source.startsWith('.') || imported.source.includes('/internal/') || + context.getGoPackageDir?.(imported.source, filePath) != null); } /** @@ -12126,7 +12132,7 @@ const PATTERN_MEMO_CAP = 8192; * ReferenceResolver.clearCaches calls clearNameMatcherMemos alongside * clearImportResolverMemos. */ -type InferScanState = { hi: number; ansIdx: number; ansType: string | null; ansRaw?: string }; +type InferScanState = { hi: number; ansIdx: number; ansType: string | null; ansRaw?: string; ansFile?: string }; const INFER_SCAN_STATES = new WeakMap>(); /** Awaited inference caches are scoped to the resolver's stable-source window. @@ -12368,8 +12374,9 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[] // are accepted only where a parameter list puts them: after `(`, `,` // or nothing, and before `,`, `)` or the line's end — unlike the // keyword-free pattern below, which a lowercase `ident type` pair in - // ordinary code would satisfy (#2323). - new RegExp(`(?:^|[(,])\\s*${r}\\s+\\*?([a-z_]\\w*)\\s*(?:\\[[^\\]]*\\])?\\s*(?:[,)]|$)`), // func (s *server) / f(h *handler) + // ordinary code would satisfy (#2323). The same slot holds a type of + // another package (`s *store.Store`, `conn net.Conn`). + new RegExp(`(?:^|[(,])\\s*${r}\\s+\\*?([a-z_]\\w*(?:\\.[A-Za-z_]\\w*)?)\\s*(?:\\[[^\\]]*\\])?\\s*(?:[,)]|$)`), // func (s *server) / f(h *handler) / f(s *store.Store) // A typed parameter / method receiver (`func use(lg Logger)`, // `func (l Logger) M()`) — name-before-type with no `var`/`:=` (#1125). // PascalCase-guarded (unlike the anchored patterns above) to keep the @@ -12911,8 +12918,10 @@ function inferLocalReceiverType( ? 0 : Math.max(0, enclosingScopeStartLine(ref, context) - 1); - // The raw capture behind matchLine's latest non-null answer (see `decl`). + // The raw capture behind matchLine's latest non-null answer (see `decl`), + // and the file it is spelled in when that is not the call's own. let lastRaw: string | undefined; + let lastFile: string | undefined; const matchLine = (i: number): string | null => { const line = lines[i]; if (!line) return null; @@ -12926,10 +12935,21 @@ function inferLocalReceiverType( const type = normalizeInferredTypeName(m[1]); if (type) { lastRaw = m[1]; + lastFile = undefined; return type; } } } + if (ref.language === 'go') { + // `r := newRing()` / `s, err := store.NewStore()`: what the callee returns. + const result = goBoundCallResult(lines, i, escapedReceiver, ref.filePath, context); + const type = result && normalizeInferredTypeName(result.raw); + if (type) { + lastRaw = result.raw; + lastFile = result.file; + return type; + } + } return null; }; @@ -12954,8 +12974,8 @@ function inferLocalReceiverType( for (let i = callIdx; i >= startIdx; i--) { const type = matchLine(i); if (type) { - states.set(key, { hi: callIdx, ansIdx: i, ansType: type, ansRaw: lastRaw }); - if (decl) decl.raw = lastRaw; + states.set(key, { hi: callIdx, ansIdx: i, ansType: type, ansRaw: lastRaw, ansFile: lastFile }); + if (decl) { decl.raw = lastRaw; decl.file = lastFile; } return type; } } @@ -12969,18 +12989,19 @@ function inferLocalReceiverType( state.ansIdx = i; state.ansType = type; state.ansRaw = lastRaw; + state.ansFile = lastFile; break; } } state.hi = callIdx; if (state.ansIdx < startIdx) return null; - if (decl) decl.raw = state.ansRaw; + if (decl) { decl.raw = state.ansRaw; decl.file = state.ansFile; } return state.ansType; } for (let i = callIdx; i >= startIdx; i--) { const type = matchLine(i); if (type) { - if (decl) decl.raw = lastRaw; + if (decl) { decl.raw = lastRaw; decl.file = lastFile; } return type; } } @@ -13444,6 +13465,11 @@ export function matchMethodCall( if (!awaited.name || TS_PRIMITIVE_TYPES.has(awaited.name)) return null; inferredType = awaited.name; } + // A Go receiver declared with a type from outside the project — `conn + // net.Conn`, `ctx context.Context`, `req *http.Request` — has no project + // method: `ctx.Done()` is not the one project type that declares a `Done`. + if (inferredType && ref.language === 'go' && decl.asserted === undefined && + isGoExternalType(decl.raw, decl.file ?? ref.filePath, context)) return null; if (inferredType) { // A Go local bound from a type assertion calls a method of the type Go // finds for it, or nothing the project declares: `flusher.Flush()` after @@ -13460,7 +13486,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) : undefined; const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType( inferredType, @@ -13990,6 +14016,95 @@ function goDeclaredTypePackage(raw: string | undefined, filePath: string, contex return goImportPackageDir(name.slice(0, dot), filePath, context) ?? undefined; } +/** + * Whether a Go type spelled `raw` in `filePath` is declared outside the + * project: a type of a package from outside the project's modules + * (`net.Conn`, `*bytes.Buffer`), or an alias of one (`type Context = + * gin.Context`). Such a type has no method in the index. + */ +function isGoExternalType(raw: string | undefined, filePath: string, context: ResolutionContext, depth = 0): boolean { + if (!raw) return false; + const name = raw.replace(/[*&\s]/g, '').replace(/\[.*$/, ''); + const dot = name.indexOf('.'); + let pkgDir: string | null | undefined = goPackageDir(filePath); + if (dot >= 0) { + const imported = context.getImportMappings(filePath, 'go').find((i) => i.localName === name.slice(0, dot)); + if (imported && isGoExternalImport(imported, filePath, context)) return true; + pkgDir = imported && context.getGoPackageDir?.(imported.source, filePath); + if (pkgDir == null) return false; + } + if (depth >= 4) return false; + const types = goPackageTypes(name.slice(dot + 1), pkgDir, context); + return types.length > 0 && types.every((t) => isGoExternalType(goAliasedType(t, context), t.filePath, context, depth + 1)); +} + +/** + * The type a Go binding gives receiver `r` from a call, when line `i` binds + * it first — `r := newRing()`, `r, err := store.NewStore(cfg)`, `if r := + * find(id); r != nil` — and the call is the whole right-hand side (`r := + * newRing().Reader()` is typed by `Reader`, which isn't read). A call that + * takes `r` itself (`r := wrap(r.Inner())`) is skipped: on that line `r` is + * still the outer variable. The type is the callee's first result as its + * signature spells it, for a function of the call's own package or of an + * imported project package; the callee itself for a conversion (`list := + * model.BotList(bots)`) and for a function of a package outside the + * project, whose result is a type from outside it too. + */ +function goBoundCallResult( + lines: string[], + i: number, + r: string, + filePath: string, + context: ResolutionContext, +): { raw: string; file?: string } | null { + const [bind, self] = memoPatterns(`go-bind|${r}`, () => [ + new RegExp(`(?:^|[;{]|\\b(?:if|switch)\\s)\\s*${r}\\s*(?:,\\s*[A-Za-z_]\\w*\\s*)*:=\\s*([A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)?)\\s*\\(`), + new RegExp(`\\b${r}\\b`), + ]); + const m = bind!.exec(lines[i]!); + if (!m) return null; + // The call's closing parenthesis, up to a few lines down, ends the statement. + let args = ''; + let closed = false; + for (let j = i, from = m.index + m[0].length - 1, depth = 0; j < Math.min(lines.length, i + 40) && !closed; j++, from = 0) { + const text = lines[j]!; + let k = from; + for (; k < text.length && !closed; k++) { + if (text[k] === '(') depth++; + else if (text[k] === ')' && --depth === 0) closed = true; + } + if (closed && !/^\s*(?:[;{}]|\/\/|$)/.test(text.slice(k))) return null; + args += text.slice(from, k) + '\n'; + } + if (!closed || self!.test(args)) return null; + + const callee = m[1]!; + const dot = callee.indexOf('.'); + const name = callee.slice(dot + 1); + let pkgDir = goPackageDir(filePath); + if (dot >= 0) { + const imported = context.getImportMappings(filePath, 'go').find((imp) => imp.localName === callee.slice(0, dot)); + if (!imported) return null; + if (isGoExternalImport(imported, filePath, context)) return { raw: callee }; + const dir = context.getGoPackageDir?.(imported.source, filePath); + if (dir == null) return null; + pkgDir = dir; + } + const fn = context.getNodesByName(name).find( + (n) => n.kind === 'function' && n.language === 'go' && goPackageDir(n.filePath) === pkgDir + ); + if (!fn) return goPackageTypes(name, pkgDir, context).length > 0 ? { raw: callee } : null; + // `(params) *T`, `(params) (*T, error)`, `(params) (t *T, err error)`. + const results = /^\s*\((?:[^()]|\([^()]*\))*\)\s*\(?\s*([^,()]*)/.exec(fn.signature ?? '')?.[1]?.trim(); + const raw = results?.split(/\s+/).pop()?.replace(/\[.*$/, ''); + if (!raw || !/^\*?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?$/.test(raw)) return null; + // A value of an outside type that project code returns is usually an + // interface a project type implements (`http.RoundTripper`, + // `health.Checker`): unknown, where a pointer (`*os.File`) is that type. + if (!raw.startsWith('*') && isGoExternalType(raw, fn.filePath, context)) return null; + return { raw, file: fn.filePath }; +} + /** The node kinds a Go `type` declaration produces. */ export const GO_TYPE_KINDS: ReadonlySet = new Set(['struct', 'interface', 'type_alias']); @@ -14065,28 +14180,38 @@ function goAliasTarget(typeNode: Node, context: ResolutionContext): { name: stri let memo = GO_ALIAS_TARGETS.get(context); if (!memo) GO_ALIAS_TARGETS.set(context, (memo = new Map())); if (memo.has(typeNode.id)) return memo.get(typeNode.id); - const lines = context.getFileLines?.(typeNode.filePath) ?? context.readFile(typeNode.filePath)?.split(/\r?\n/) ?? []; - // From the alias's name, where the node starts, to the end of its type. - const decl = lines - .slice(Math.max(0, typeNode.startLine - 1), typeNode.endLine ?? typeNode.startLine) - .map((l, i) => (i === 0 ? l.slice(typeNode.startColumn ?? 0) : l).replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '')) - .join(' '); - // `Name =` or `Name[T any] =`; a defined type has no `=` there. - const head = /^\s*[A-Za-z_]\w*\s*(?:\[[^\]]*\])?\s*=/.exec(decl); + const aliased = goAliasedType(typeNode, context); // `T`, `*T`, `pkg.T`, each perhaps with type arguments. - const m = head && /^\s*(?:\*\s*)?([A-Za-z_]\w*)(?:\s*\.\s*([A-Za-z_]\w*))?\s*(?:\[.*\])?\s*;?\s*$/.exec(decl.slice(head[0].length)); + const m = aliased !== undefined && /^\s*(?:\*\s*)?([A-Za-z_]\w*)(?:\s*\.\s*([A-Za-z_]\w*))?\s*(?:\[.*\])?\s*;?\s*$/.exec(aliased); let target: { name: string; pkgDir: string } | null | undefined; if (m) { const pkgDir = m[2] ? goImportPackageDir(m[1]!, typeNode.filePath, context) : goPackageDir(typeNode.filePath); const name = m[2] ?? m[1]!; target = pkgDir == null || (!m[2] && GO_BUILTIN_FIELD_TYPES.has(name)) ? null : { name, pkgDir }; } else { - target = head ? null : undefined; + target = aliased !== undefined ? null : undefined; } memo.set(typeNode.id, target); return target; } +/** + * What a Go alias declaration writes after its `=` (`context.Context`, + * `*Local`, `func()`). Undefined when the node is no alias. + */ +function goAliasedType(typeNode: Node, context: ResolutionContext): string | undefined { + if (typeNode.kind !== 'type_alias') return undefined; + const lines = context.getFileLines?.(typeNode.filePath) ?? context.readFile(typeNode.filePath)?.split(/\r?\n/) ?? []; + // From the alias's name, where the node starts, to the end of its type. + const decl = lines + .slice(Math.max(0, typeNode.startLine - 1), typeNode.endLine ?? typeNode.startLine) + .map((l, i) => (i === 0 ? l.slice(typeNode.startColumn ?? 0) : l).replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '')) + .join(' '); + // `Name =` or `Name[T any] =`; a defined type has no `=` there. + const head = /^\s*[A-Za-z_]\w*\s*(?:\[[^\]]*\])?\s*=/.exec(decl); + return head ? decl.slice(head[0].length) : undefined; +} + /** The declarations of Go type `typeName` in the package at directory `pkgDir`. */ function goPackageTypes(typeName: string, pkgDir: string, context: ResolutionContext): Node[] { return context.getNodesByName(typeName).filter( @@ -14196,13 +14321,16 @@ interface GoProjectType { /** * What inferLocalReceiverType tells its caller besides the type: the type as - * the declaration spells it (`raw`), and for a Go local bound from a type - * assertion, the type goWrittenType finds for it (`asserted`; null for one - * with no method the project declares). `asserted` is unset when the local - * is declared some other way, or goWrittenType can't tell its type. + * the declaration spells it (`raw`), the file that spelling is read in when + * it is not the call's own (`file` — a callee's result type is written in the + * callee's file), and for a Go local bound from a type assertion, the type + * goWrittenType finds for it (`asserted`; null for one with no method the + * project declares). `asserted` is unset when the local is declared some + * other way, or goWrittenType can't tell its type. */ interface GoReceiverDecl { raw?: string; + file?: string; asserted?: GoProjectType | null; } @@ -14511,6 +14639,8 @@ function matchGoFieldChainCall( // whose fields the project doesn't declare. const asserted = decl.asserted; if (asserted === null) return null; + const declFile = decl.file ?? ref.filePath; + if (!asserted && isGoExternalType(decl.raw, declFile, context)) return null; const fieldEsc = field!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`); @@ -14523,7 +14653,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, declFile, context); const declared = structs.filter((n) => goPackageDir(n.filePath) === basePkg); if (declared.length > 0 || asserted) structs = declared; structs = preferCallSiteFile(structs, ref.filePath);