fix(go): a call through a variable declared with another package's type reaches that type's method (#2706)

Go receiver inference read a parameter's type only when the line
patterns could: a lowercase name of the caller's own package (`s
*server`) or a capitalized one (`lg Logger`). A parameter declared
through a package, like `client rest.Interface`, `c clock.PassiveClock`
or `t *testing.T`, had no type, so a call on it was matched by name:
kubernetes' `client.Patch()` reached `RESTClient::Patch` instead of the
interface, `c.Now()` an unrelated `Clock`, and every `t.Errorf(…)` a
test helper's `T` interface. Only standard method names (PR 2664) and
receivers named like an import (PR 2448) asked the scope reader.

inferLocalReceiverType now asks the scope reader first for every Go
receiver. The declaration in scope (a parameter, a result, a name
grouped with others, a `var (…)` spec, a function literal's parameter)
gives the named type it writes, as GoReceiverDecl.declared, ahead of
the patterns, which scan back with no block scope. matchMethodCall,
matchMemberFunctionRef and matchGoFieldChainCall all use it:
- a project type resolves in its own package, through embedding and
  aliases;
- a type from outside the project, a predeclared one or an alias of
  either (goHasNoProjectMethods) reaches none of the project's methods,
  and neither does a type literal;
- a qualifier none of the file's imports names keeps the old lookup by
  name.
Standard method names keep the rules PR 2664 gave them. Any other name
the declared project type lacks goes on to the name strategies, as on a
receiver of unread type. The block that typed only receivers named
like an import is subsumed and removed, with the field it read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 16:38:24 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 70783f31f1
commit 6c49bfa667
4 changed files with 409 additions and 54 deletions
+1
View File
@@ -160,6 +160,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Go, a call is now matched to the package or variable it is actually made on. Before, a call through a package, like `klog.Infof("Log using Infof")` or `digest.Digest(dig)` inside `&distribution.Descriptor{Digest: …}`, could link to a project method of the same name when the name appeared again on the line, and in `klog.Error(err.Error())` the `err.Error()` call was taken for a call into `klog`. A parameter, receiver or local variable named like an import, like etcd's `jwt, err := newTokenProviderJWT(…)` or the `suite` receiver in a testify test suite, now counts as that variable, so calls on it link to the methods of the type it is declared as, and to nothing when that type comes from outside your project. Re-index Go projects after upgrading.
- In Go, a call through a parameter or local variable named like a standard-library package, such as `context`, `log`, `trace`, `printer` or `token`, now links to the method it calls. Before, every call written `context.…` or `printer.…` was taken for a call into the standard library and linked to nothing: gin's `context.AbortWithError(…)` inside `func(context *Context)`, harbor's `context.IsAuthenticated()` after `context := NewSecurityContext(…)` and `log.Errorf(…)` after `log := log.G(ctx)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`. The variable's declaration decides the method: the type it is declared as, or the package of the project function its value comes from. A variable holding a value from outside your project, like `scanner := bufio.NewScanner(r)` or `url, err := url.Parse(raw)`, still links to nothing, and one whose type can't be told links only to a method no other type in the project has, never to one picked because it matches the variable's name. Re-index Go projects after upgrading.
- In Go, a call of a method name that nearly every type has, like `err.Error()`, `id.String()`, `ctx.Err()` or `mu.Lock()`, now links to that method of the type its variable is declared as, and to nothing when that type is built into Go, like `error` or `string`, or comes from outside your project, like `context.Context`, `sync.Mutex` or `testing.TB`. Before, when the variable's type wasn't worked out, such a call was matched by a word its name shares with a type's, so kubernetes' `err.Error()` calls linked to an unrelated `notRegisteredErr` type and etcd's `leaderID.String()` to `MoveLeaderRequest`. A call on a variable whose type still can't be told now links only to a type the variable is named after, like `id` for an `ID`. Re-index Go projects after upgrading.
- In Go, a call on a parameter or variable declared with a type from another package, like `client rest.Interface`, `app storage.Appender` or `pod *v1.Pod`, now links to that type's method, including one it gets from a type it embeds or one an alias names, and to nothing when the type comes from outside your project, like `w http.ResponseWriter`, `c clock.PassiveClock` or an alias of such a type. Before, the type was read only for a few method names that nearly every type has, so other calls were matched by name: `client.Patch(…)` linked to a `RESTClient` rather than the interface it is declared as, `c.Now()` to an unrelated `Clock`, and a `sync.Pool` or `http.Client` variable's calls to a project type of that name. Names declared together, like `a, b rest.Interface`, `var ( … )` blocks, named results and function literal parameters are read the same way, always from the declaration in scope rather than another variable of that name further up, like a test table's field. Re-index Go projects after upgrading. Thanks @danusha2345, whose #1954 found the same gap.
- In Go, a method call on a variable or constant declared at the top level of a package, like `lg.Panic(…)` after `var lg *zap.Logger` or `RoleEndpoint.String()` after `RoleEndpoint Role = "endpoints"`, now links to that method of the type it is declared with, from any file of the package, and to nothing when that type, or the value it is set to, comes from outside your project. Before, nothing read such a declaration, so the call was matched by its method's name alone and could link to an unrelated type's method, like a project logger's `Panic` or another package's `Role`. Re-index Go projects after upgrading.
- React Router routes written as JSX now read `<Route index>` the way route objects do: inside `<Route path="/" element={<Layout />}>`, `<Route index element={<Home />} />` is the page at `/`, and `Layout` counts as the layout of every route nested in it, as does a guard with no path of its own like `<Route element={<RequireAuth />}>`. Before, the index route was never read, so `/` led to the layout itself and a `<Route path="/dashboard">` holding only an index route led to nothing. A route's `element` written on lines of its own, the way Prettier formats a long one, or wrapped in `<Suspense fallback={<Loader />}>`, now links to the page it shows rather than to nothing or to the fallback, and a path written in braces, like `path={"agents"}`, is read like any other. Re-index React projects after upgrading.
- In JavaScript and TypeScript, a module loaded with `require('./x').default` or `const { default: X } = require('./x')` now links to its default export when it is written as an ES module: bitwarden's desktop app loads `export default class OsBiometricsServiceMac` this way before calling `new OsBiometricsServiceMac(…)`, and React Native's own libraries load many of their modules like this. Before, `.default` was looked up only as an export named `default`, which `export default` is not, so the `new` and the calls made through it linked to nothing, to the local variable holding the module, or to a method of the same name somewhere else. A Svelte component loaded this way and created with `new App({ target })` now links to the component. A CommonJS module that sets `exports.default` itself, or both `module.exports` and `module.exports.default` as fastify does, still links to what it sets, and one that sets no `default` still links to nothing. Re-index JavaScript and TypeScript projects after upgrading.
+331
View File
@@ -0,0 +1,331 @@
/**
* A Go receiver's declared type decides which method a call on it reaches.
* The line patterns that read it take a parameter's type only when it is a
* lowercase name of the caller's own package (`s *server`) or starts with a
* capital (`lg Logger`), so a parameter declared through a package — `c
* clock.PassiveClock`, `pod *v1.Pod`, `client kubernetes.Interface` — had
* no type, and a call on it went to whatever project method a guess by name
* found: kubernetes' `c.Now()` reached apimachinery's unrelated `Clock::Now`,
* `client.Patch()` a `RESTClient` instead of the `rest.Interface` declared.
* Only a call of a standard method name (`String`, `Error`, `Header`) was
* read through the scope reader. Now every call is: a parameter, a result,
* a name sharing its type with the next (`a, b rest.Interface`), a `var (…)`
* spec, a function literal's parameter. The patterns scan back without
* block scope, so the scope reader's declaration wins where both find one.
*
* A type from a package outside the project has none of the project's
* methods, and neither has an alias of one. gin declares its own
* `ResponseWriter`, which `w http.ResponseWriter` is not.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const files: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
'rest/rest.go': `package rest
type Request struct{}
// Interface is what a client of the API server offers.
type Interface interface {
Patch(pt string) *Request
Get() *Request
}
// RESTClient is one implementation of it: a call through Interface is not a call on it.
type RESTClient struct{}
func (c *RESTClient) Patch(pt string) *Request { return &Request{} }
func (c *RESTClient) Get() *Request { return &Request{} }
`,
'store/store.go': `package store
type Store struct{}
func (s *Store) Put(k string) {}
func (s *Store) Get(k string) string { return k }
// Cached gets Put and Get from the Store it embeds.
type Cached struct {
*Store
}
`,
'api/api.go': `package api
import "example.com/app/store"
// Alias is store.Store under another name.
type Alias = store.Store
`,
'config/config.go': `package config
import "example.com/app/store"
type Config struct {
Store *store.Store
}
`,
// Methods named like the ones the calls below make, for a guess by name to find.
'decoy/decoy.go': `package decoy
import "net/http"
type Decoy struct{}
func New() *Decoy { return &Decoy{} }
func (d *Decoy) Put(k string) {}
func (d *Decoy) Get(k string) string { return k }
// Like gin's own ResponseWriter.
type ResponseWriter interface {
Header() http.Header
Write(b []byte) (int, error)
}
type responseWriter struct{}
func (w *responseWriter) Header() http.Header { return nil }
func (w *responseWriter) Write(b []byte) (int, error) { return len(b), nil }
// Like harbor's jobservice worker.
type Worker struct{}
func (w *Worker) WithContext(ctx interface{}) *Worker { return w }
// Like apimachinery's clock.
type Clock struct{}
func (c *Clock) Now() int64 { return 0 }
// A struct named like the handlers package's alias of context.Context.
type Ctx struct{}
func (c *Ctx) Done() <-chan struct{} { return nil }
// An HTTP client of the project's own.
type Client struct{}
func (c *Client) Do(req *http.Request) error { return nil }
// A request of the project's own, whose URL has a Query.
type Request struct {
URL *Location
}
type Location struct{}
func (l *Location) Query() string { return "" }
`,
'handlers/handlers.go': `package handlers
import (
"context"
"net/http"
"example.com/app/api"
"example.com/app/config"
"example.com/app/decoy"
"example.com/app/rest"
"example.com/app/store"
"k8s.io/utils/clock"
)
// Ctx is context.Context under another name.
type Ctx = context.Context
// Local is the type of a test table's field below.
type Local struct{}
func (l *Local) Put(k string) {}
func update(client rest.Interface) {
client.Patch("json")
}
func pair(a, b rest.Interface) {
a.Get()
b.Patch("merge")
}
func multiLine(
ctx context.Context,
s *store.Store,
) {
s.Put("multi")
}
func open() (s *store.Store) {
s.Put("result")
return s
}
func block() {
var (
s *store.Store
err error
)
s.Put("block")
_ = err
}
func closure() {
put := func(s *store.Store) {
s.Put("closure")
}
_ = put
}
func table(s *store.Store) {
cases := []struct {
s *Local
}{}
_ = cases
s.Put("table")
}
func flushAll(c *store.Cached) {
c.Put("embedded")
}
func viaAlias(a api.Alias) {
a.Get("alias")
}
func value(s *store.Store) {
put := s.Put
_ = put
}
func chain(cfg *config.Config) {
cfg.Store.Put("chain")
}
func shadow(s *store.Store) {
if s != nil {
s := decoy.New()
s.Put("shadow")
}
}
func serve(w http.ResponseWriter, writer http.ResponseWriter, req *http.Request) {
w.Header()
writer.Header()
req.WithContext(context.Background())
req.URL.Query()
}
func tick(c clock.PassiveClock) {
c.Now()
}
func wait(ctx context.Context, c Ctx) {
ctx.Done()
c.Done()
}
func send() {
var rw http.ResponseWriter
rw.Write(nil)
cl := &http.Client{}
cl.Do(nil)
}
func now(realClock clock.PassiveClock) {
tick := realClock.Now
_ = tick
}
`,
};
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-declared-receiver-'));
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 });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const FILE = 'handlers/handlers.go';
/** The line of the handlers file holding `snippet`. */
const lineOf = (snippet: string): number => {
const at = files[FILE]!.split('\n').findIndex((l) => l.includes(snippet));
if (at < 0) throw new Error(`no ${snippet} in ${FILE}`);
return at + 1;
};
/** What the references on the line holding `snippet` reach: `owner::method (resolvedBy confidence)`. */
const reachedAt = (snippet: string): string[] => {
const line = lineOf(snippet);
const ids = cg.getNodesInFile(FILE).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind !== 'contains' && e.line === line)
.map((e) => `${cg.getNode(e.target)!.qualifiedName} (${e.metadata?.resolvedBy} ${e.metadata?.confidence})`);
};
describe('a Go receiver declared through a package', () => {
it('reaches the method of the type it is declared as', () => {
// Not RESTClient's `Patch`, which a word of the receiver's name picked.
expect(reachedAt('client.Patch("json")')).toEqual(['Interface::Patch (instance-method 0.9)']);
// Each name of a group, a line of a multi-line list, a named result, a
// `var (…)` spec, a function literal's parameter.
expect(reachedAt('a.Get()')).toEqual(['Interface::Get (instance-method 0.9)']);
expect(reachedAt('b.Patch("merge")')).toEqual(['Interface::Patch (instance-method 0.9)']);
expect(reachedAt('s.Put("multi")')).toEqual(['Store::Put (instance-method 0.9)']);
expect(reachedAt('s.Put("result")')).toEqual(['Store::Put (instance-method 0.9)']);
expect(reachedAt('s.Put("block")')).toEqual(['Store::Put (instance-method 0.9)']);
expect(reachedAt('s.Put("closure")')).toEqual(['Store::Put (instance-method 0.9)']);
});
it('is the declaration in scope, not a test table field the patterns read above it', () => {
expect(reachedAt('s.Put("table")')).toEqual(['Store::Put (instance-method 0.9)']);
});
it('follows embedding and aliases in the declaring package', () => {
expect(reachedAt('c.Put("embedded")')).toEqual(['Store::Put (instance-method 0.9)']);
expect(reachedAt('a.Get("alias")')).toEqual(['Store::Get (instance-method 0.9)']);
});
it('types a method value and a field chain through it too', () => {
expect(reachedAt('put := s.Put')).toEqual(['Store::Put (function-ref 0.9)']);
expect(reachedAt('cfg.Store.Put("chain")')).toEqual(['Store::Put (instance-method 0.85)']);
});
it('is not what a local of the same name declared without a type holds', () => {
// `s := decoy.New()` shadows the parameter: its type is not *store.Store.
expect(reachedAt('s.Put("shadow")').filter((r) => r.startsWith('Store::'))).toEqual([]);
});
});
describe('a Go receiver of a type from outside the project', () => {
it('reaches none of the project\'s methods', () => {
// net/http's ResponseWriter is not the project's own, whose name it shares.
expect(reachedAt('w.Header()')).toEqual([]);
expect(reachedAt('writer.Header()')).toEqual([]);
expect(reachedAt('req.WithContext(')).toEqual([]);
expect(reachedAt('req.URL.Query()')).toEqual([]);
expect(reachedAt('c.Now()')).toEqual([]);
expect(reachedAt('ctx.Done()')).toEqual([]);
});
it('nor does an alias of one, or a local declared as one', () => {
expect(reachedAt('c.Done()')).toEqual([]);
expect(reachedAt('rw.Write(nil)')).toEqual([]);
expect(reachedAt('cl.Do(nil)')).toEqual([]);
// A method value of one, which a word of its name picked on main.
expect(reachedAt('tick := realClock.Now')).toEqual([]);
});
});
+2 -2
View File
@@ -318,8 +318,8 @@ describe('Go standard method names on a receiver nothing types', () => {
it.each([
// `var ( query promql.Query )`, read past the table's `query string`;
// `Exec` is no standard name and is guessed as before.
['runQueries', ['promql/engine.go::Query::Exec instance-method@0.8', 'promql/engine.go::Query::String instance-method@0.9']],
// `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']],
+75 -52
View File
@@ -313,6 +313,9 @@ function matchMemberFunctionRef(ref: UnresolvedRef, context: ResolutionContext):
if (decl.asserted !== undefined) {
return decl.asserted && resolveMethodOnType(decl.asserted.name, member, ref, context, 0.9, 'function-ref', decl.asserted.dir);
}
// 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;
if (type) {
return resolveMethodOnType(type, member, ref, context, 0.9, 'function-ref',
goDeclaredTypePackage(decl.raw, ref.filePath, context));
@@ -2294,13 +2297,11 @@ interface GoQualification {
imported?: ImportMapping;
/**
* For a call through a parameter or local that takes an import's name, the
* type it is declared as, when its declaration writes one (`*tokenJWT`,
* `clock.PassiveClock`), and the import that type is written through —
* `clock` in a parameter `clock clock.PassiveClock`, `atomic` in
* `cache := &atomic.Bool{}` — or its value comes from: `reflect` in
* `value := reflect.New(t)`.
* import the type its declaration writes is written through — `clock` in a
* parameter `clock clock.PassiveClock`, `atomic` in `cache :=
* &atomic.Bool{}` — or its value comes from: `reflect` in `value :=
* reflect.New(t)`.
*/
declaredType?: string;
typePackage?: ImportMapping;
}
@@ -2337,7 +2338,6 @@ function goRefQualification(ref: UnresolvedRef, context: ResolutionContext): GoQ
}
const imports = context.getImportMappings(ref.filePath, 'go');
let imported = written ? imports.find((m) => m.localName === written) : undefined;
let declaredType: string | undefined;
let typePackage: ImportMapping | undefined;
// A call through a parameter or local that takes the import's name is a
// call on that variable: etcd's `jwt, err := newTokenProviderJWT(…)`, then
@@ -2347,16 +2347,15 @@ function goRefQualification(ref: UnresolvedRef, context: ResolutionContext): GoQ
// (`quota.Name`) is no call.
const local = imported && dot >= 0 && ref.referenceKind === 'calls' ? goLocalDecl(written!, ref, context) : undefined;
if (local) {
declaredType = local.type;
// A value a package outside the project hands out is of a type it
// declares: kubernetes' `clock := clocktesting.NewFakePassiveClock(…)`
// has no project method `Now`.
const qualifier = /^[*&\s]*([A-Za-z_]\w*)\.[A-Za-z_]\w*$/.exec(declaredType ?? '')?.[1] ?? local.from;
const qualifier = /^[*&\s]*([A-Za-z_]\w*)\.[A-Za-z_]\w*$/.exec(local.type ?? '')?.[1] ?? local.from;
typePackage = qualifier ? imports.find((m) => m.localName === qualifier) : undefined;
written = undefined;
imported = undefined;
}
const found = { written, imported, declaredType, typePackage };
const found = { written, imported, typePackage };
memo.set(key, found);
return found;
}
@@ -2457,6 +2456,21 @@ function isGoOutsideType(written: string | undefined, filePath: string, context:
return !!qualifier && isGoOutsidePackage(qualifier, filePath, context);
}
/**
* Whether a Go named type a declaration at `ref` writes (`goNamedType`) has
* none of the project's methods: a type from outside the project
* (`http.ResponseWriter`), a predeclared one, or an alias of such a type
* (`type Ctx = context.Context`). Not one written through a qualifier none
* of the file's imports names as indexed, or an unmapped internal package,
* which can't be told.
*/
function goHasNoProjectMethods(named: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (isGoOutsideType(named, ref.filePath, context)) return true;
const dot = named.indexOf('.');
if (dot >= 0 && !goImportPackageDir(named.slice(0, dot), ref.filePath, context)) return false;
return goWrittenType(named, ref, context) === null;
}
/** Whether `name` is the file's import of a package from outside the project: `context` in `context.WithTimeout(…)`. */
function isGoOutsidePackage(name: string, filePath: string, context: ResolutionContext): boolean {
const imp = context.getImportMappings(filePath, 'go').find((m) => m.localName === name);
@@ -15087,6 +15101,19 @@ export function inferLocalReceiverType(
decl.asserted = asserted;
return normalizeInferredTypeName(written!) ?? written!;
}
// A Go parameter or local whose declaration in scope writes its type has
// that type: one declared through a package (`c clock.PassiveClock`, `pod
// *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;
if (type !== undefined) {
const named = goNamedType(type);
decl.declared = named;
if (!named) return null;
decl.raw = named;
return named.slice(named.indexOf('.') + 1);
}
}
const escapedReceiver = scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -15644,40 +15671,23 @@ export function matchMethodCall(
// A pytest test's parameter is what its fixture returns: flaskbb's
// `cli_runner.invoke(…)` is click's `CliRunner`, not the project's one `invoke`.
if (!inferredType && ref.language === 'python' && dotMatch) inferredType = pythonFixtureReturnType(objectOrClass!, ref, context);
// A Go parameter named like an import is declared through a package as
// often as not (`logger logger.Interface`), which the patterns above
// don't read; the scope reader kept its type. Not a field's: in
// `metadata.Type.String()` that is the type of `metadata`.
if (!inferredType && ref.language === 'go' && dotMatch && !objectOrClass!.includes('.')) {
const declared = goRefQualification(ref, context).declaredType;
if (declared) {
inferredType = normalizeInferredTypeName(declared);
decl.raw = declared;
}
}
// A Go standard method name says nothing of its receiver, so the
// receiver's declaration in scope, which the scope reader finds, decides:
// etcd's parameter `id types.ID` makes `id.String()` `ID::String`,
// prometheus's `iter chunks.Iterator` makes `iter.Err()` that interface's
// rather than `StringIter::Err`, and a value a package from outside the
// project hands out (`ctx, cancel := context.WithTimeout(…)`) has no
// project method. It overrides the patterns above, which scan back for
// the name without block scope and can read another declaration of it:
// A Go receiver's declaration in scope, as the scope reader finds it
// (`declared`), decides over the patterns, which scan back for the name
// without block scope and can read another declaration of it:
// prometheus's `var ( query promql.Query )` came out as its test table's
// `query string`. Only a type read here rules a call out below.
let goDeclared: string | undefined;
if (goStd && decl.asserted === undefined) {
const local = goLocalDecl(objectOrClass!, ref, context);
if (local?.type) {
const named = goNamedType(local.type);
// A slice, map, channel, function or interface literal type has no
// project method.
if (!named) return null;
inferredType = named.split('.').pop()!;
decl.raw = goDeclared = named;
} else if (local?.from && isGoOutsidePackage(local.from, ref.filePath, context)) {
return null;
}
// `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;
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()`
// `ID::String`, prometheus's `iter chunks.Iterator` makes `iter.Err()`
// that interface's rather than `StringIter::Err`, and a value a package
// from outside the project hands out (`ctx, cancel :=
// context.WithTimeout(…)`) has no project method.
if (goStd && goDeclared === undefined && decl.asserted === undefined) {
const from = goLocalDecl(objectOrClass!, ref, context)?.from;
if (from && isGoOutsidePackage(from, ref.filePath, context)) return null;
}
if (!inferredType && MEMBER_TYPED_LANGUAGES.has(ref.language) && dotMatch) {
inferredType = nmTimedT('mc-member', ref, () => inferMemberReceiverType(objectOrClass!, ref, context));
@@ -15744,12 +15754,13 @@ export function matchMethodCall(
if (decl.asserted !== undefined) {
return decl.asserted && resolveMethodOnType(decl.asserted.name, methodName!, ref, context, 0.9, 'instance-method', decl.asserted.dir);
}
// A standard method name on a Go receiver declared as a type from a
// package outside the project (`var mu sync.Mutex`, `tb testing.TB`) is
// that type's own method, never a same-named project type's: etcd's
// `pendingMu.Lock()` went to its concurrency `Mutex`, kubernetes'
// `tb.Error(…)` to a feature-gate test helper's `TB`.
if (goDeclared !== undefined && isGoOutsideType(goDeclared, ref.filePath, context)) return null;
// A Go receiver declared as a type from a package outside the project
// (`var mu sync.Mutex`, `w http.ResponseWriter`), or as an alias of
// one, calls that type's own method, never a same-named project type's:
// etcd's `pendingMu.Lock()` went to its concurrency `Mutex`, kubernetes'
// `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;
// 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.
@@ -15824,8 +15835,11 @@ export function matchMethodCall(
// no other method on a predeclared type, so a predeclared type the
// patterns read for another name's declaration, like a test table's
// `version string` above kubernetes' `version, _ := parse(…)`, rules
// nothing out.)
if (goDeclared !== undefined) return null;
// nothing out.) Any other name the declared type lacks goes on to the
// 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;
// The receiver's declared type is one the project doesn't declare —
// `List<Roshambo> list`, `String s`, `val sb = StringBuilder()` — so the
// method is that outside type's. gson's `list.add(…)` went to a project
@@ -16577,10 +16591,14 @@ interface GoProjectType {
* 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.
* `declared` is the named type a Go parameter or local's declaration in
* scope writes, as the scope reader finds it (`clock.PassiveClock`, `Store`),
* or null for a type literal; unset when that declaration writes no type.
*/
interface GoReceiverDecl {
raw?: string;
asserted?: GoProjectType | null;
declared?: string | null;
}
const GO_ASSERTION_BINDINGS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
@@ -17022,9 +17040,12 @@ function matchGoFieldChainCall(
const baseType = inferLocalReceiverType(base!, ref, context, decl);
if (!baseType) return null;
// A base bound from a type assertion has the type Go finds for it, or one
// whose fields the project doesn't declare.
// whose fields the project doesn't declare, as does one declared as a type
// from outside the project: `req.URL.Query()` on an `*http.Request` is no
// project `Request`'s.
const asserted = decl.asserted;
if (asserted === null) return null;
if (decl.declared && goHasNoProjectMethods(decl.declared, ref, context)) return null;
const fieldEsc = field!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`);
@@ -17075,6 +17096,8 @@ function matchGoFieldChainCall(
// validated `<type>::<method>` match.
const fieldType = rawType.split('.').pop();
if (!fieldType || !/^[A-Za-z_]/.test(fieldType) || GO_BUILTIN_FIELD_TYPES.has(fieldType)) continue;
// Nor is an alias of an outside type (`ctx Ctx` with `type Ctx = context.Context`).
if (goHasNoProjectMethods(rawType, { ...ref, filePath: s.filePath }, context)) continue;
const resolved = resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method', pkgDir);
if (resolved) return resolved;
}