Files
codegraph/__tests__/go-std-method-receivers.test.ts
T
5594843698 fix(go): a call on a value bound from a call or a conversion reaches its type's method (#2961)
A Go method call on a local bound from a call or a conversion, like
`r := newRing(); r.Reset()`, `v1 := router.Group(...); v1.GET(...)` or
`c, r := CreateTestContext(w); r.Use(...)`, was matched by the method's
name alone: inferLocalReceiverType reads a declaration's written type and a
few line patterns, none of which reads a call's result. The by-name guess
took a namesake (etcd's `lg := s.Logger()` linked `lg.Info(...)` to a
project logger's Info, prometheus's `NewWriteHandler(...)` results to the
read handler's ServeHTTP), or found nothing.

Main already types a value from its declaration for indexed and chained
calls (goDeclaredValueType, #2782). Plain receivers now use it too: a local
whose in-scope declaration has a call or a conversion as its value takes
that value's type (the k-th result of several, a method's result, a
conversion's type), read in the file that writes it (GoReceiverDecl.file),
for matchMethodCall, matchMemberFunctionRef and matchGoFieldChainCall. A
value of a type from outside the project links nothing, including one a
project function returns as an outside interface. Assertions stay with
goAssertedLocalType, and a type switch's guard is not typed from its value.

The typer itself, for every path that uses it:
- a trailing `.(T)` is a T whatever its prefix (`pool.Get().(*Conv)`);
- a call through a func-typed field gives the field's result type;
- a bare call a dot import brings in is typed (`package watch_test`
  calling `NewStreamWatcher(...)`);
- a method of a struct or interface literal, of an outside generic type
  (grpc's `ServerStreamingClient[Resp].Recv()`) or one the index doesn't
  show on a project type gives an unknown type, not "outside", so name
  matching runs as before.

Validation:
- go-receiver-type.test.ts (the PR's 23 cases, 2 expectations updated where
  the call is now typed, plus 4 ours), go-std-method-receivers.test.ts and
  go-chained-receivers.test.ts: on main 19 of 51 fail; all pass here. All
  45 go-*.test.ts files pass (491 tests).
- A/B vs main 25776014 (same kernel): gin +194/-52/65 moved, etcd
  +4,441/-359/743, prometheus +7,041/-239/1,409, a kubernetes staging
  subtree +1,154/-86/354, harbor without its portal +423/-138/366.
  express and flask byte-identical. Sampled additions right; removals are
  outside values (zap, os, grpc, otel, beego, pflag) and the outside-
  interface results, about 80% of which were wrong by sample. Known loss:
  a type defined over an interface (`type Codec Serializer`) has no methods
  in the index, so a coincidental guess on such a value goes, as in #2706.
- Compared with PR #1954 on the same repos: 4,962 more links and 360 of
  its edges gone, those mostly wrong guesses (166 of them the outside-
  interface results); its declaration-line restriction also lost right
  links on range variables and field values, which this leaves to main's
  scan.
- CPU (profile self time of receiver typing): prometheus 1.82 -> 1.72 s,
  k8s subtree 0.88 -> 0.92 s, harbor 1.02 -> 0.86 s.

Reimplements PR #1954 by @danusha2345 on main's value typer; its tests are
included.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:56:46 +00:00

341 lines
9.3 KiB
TypeScript

/**
* Go calls of a standard method name — `Error`, `String`, `Err`, `Lock`,
* `Header` — on a receiver nothing types.
*
* Every Go type may carry such a name, so the name alone says nothing of the
* receiver. A guess by a word the receiver shares with a type's name bound
* kubernetes' `err.Error()` to `notRegisteredErr::Error` and etcd's
* `ctx.Err()` to a `valCtx`. The receiver's declaration decides when it can be
* read — its declared type's own method, or nothing for a predeclared type
* or a type from outside the project — and otherwise only a receiver named
* after the owner type itself gets a guess.
*/
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';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-std-recv-'));
const files: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
'types/id.go': `package types
type ID uint64
func (i ID) String() string { return "" }
type URLsMap map[string][]string
func NewURLsMap() URLsMap { return URLsMap{} }
func (m URLsMap) String() string { return "" }
`,
'runtime/error.go': `package runtime
type notRegisteredErr struct{}
func (e *notRegisteredErr) Error() string { return "" }
type ParseErr struct{}
func (e *ParseErr) Error() string { return "" }
`,
'etcdserver/request.go': `package etcdserver
type MoveLeaderRequest struct{}
func (m *MoveLeaderRequest) String() string { return "" }
`,
'client/watch.go': `package client
import "context"
type valCtx struct{ context.Context }
func (c *valCtx) Err() error { return nil }
`,
'concurrency/mutex.go': `package concurrency
type Mutex struct{}
func (m *Mutex) Lock() error { return nil }
func (m *Mutex) Unlock() error { return nil }
`,
'chunks/iterator.go': `package chunks
type Iterator interface {
Next() bool
Err() error
}
`,
'index/postings.go': `package index
type StringIter interface {
Next() bool
Err() error
}
type symbolsIter struct{}
func (s *symbolsIter) Next() bool { return false }
func (s *symbolsIter) Err() error { return nil }
`,
'featuregate/fgtesting/tb.go': `package fgtesting
type TB interface {
Error(args ...any)
}
`,
'kubelet/runtime.go': `package kubelet
type Status struct{ Code int }
type RuntimeStatus struct{}
func (r *RuntimeStatus) String() string { return "" }
`,
'httpx/recorder.go': `package httpx
import "net/http"
type ResponseRecorder struct{}
func (r *ResponseRecorder) Header() http.Header { return nil }
`,
'boot/srv.go': `package boot
type Srv struct{}
func (s *Srv) Start() {}
`,
'promql/engine.go': `package promql
type Query interface {
Exec() string
String() string
}
func NewQuery(q string) Query { return nil }
`,
'server/version.go': `package server
type Version struct{}
func ParseVersion(s string) (*Version, error) { return &Version{}, nil }
func (v *Version) Major() string { return "" }
func (v *Version) String() string { return "" }
`,
'server/server.go': `package server
import (
"context"
"fmt"
"net/http/httptest"
"sync"
"testing"
"time"
"example.com/app/boot"
"example.com/app/chunks"
"example.com/app/promql"
"example.com/app/types"
)
type Server struct{}
func (s *Server) String() string { return "" }
func (s *Server) Describe() string { return s.String() }
func doWork() error { return nil }
func lookupLeader() fmt.Stringer { return nil }
func currentStatus() fmt.Stringer { return nil }
func newSrv() *boot.Srv { return nil }
func errorText(err error) string { return err.Error() }
func workText() string {
err := doWork()
return err.Error()
}
func leaderText() string {
leaderID := lookupLeader()
return leaderID.String()
}
func memberText(memberID types.ID) string { return memberID.String() }
func idsText(ids []types.ID) string {
out := ""
for _, id := range ids {
out += id.String()
}
return out
}
func ctxErr(ctx context.Context) error { return ctx.Err() }
func timeoutErr() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
return ctx.Err()
}
func locked() {
var mu sync.Mutex
mu.Lock()
}
func iterErr(iter chunks.Iterator) error { return iter.Err() }
func report(tb testing.TB) { tb.Error("failed") }
func statusText() string {
status := currentStatus()
return status.String()
}
func headers() {
recorder := httptest.NewRecorder()
_ = recorder.Header()
}
func urls() string {
urlsMap := types.NewURLsMap()
return urlsMap.String()
}
func startSrv() {
srv := newSrv()
srv.Start()
}
func runQueries() {
for _, tc := range []struct {
query string
}{{query: "up"}} {
func() {
var (
query promql.Query
)
query = promql.NewQuery(tc.query)
_ = query.Exec()
_ = query.String()
}()
}
}
func versionText() string {
tests := []struct {
version string
}{{version: "1.0"}}
out := ""
for _, test := range tests {
version, _ := ParseVersion(test.version)
out += version.Major() + version.String()
}
return out
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
}, 120_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `file::qualifiedName resolvedBy@confidence` of every method the function `name` in server.go calls. */
function calls(name: string): string[] {
const fn = cg.getNodesInFile('server/server.go').find((n) => n.name === name && n.kind !== 'method');
expect(fn, name).toBeDefined();
return cg
.getOutgoingEdges(fn!.id)
.filter((e) => e.kind === 'calls')
.map((e) => ({ e, n: cg.getNode(e.target)! }))
.filter(({ n }) => n.kind === 'method')
.map(({ e, n }) => `${n.filePath.replace(/\\/g, '/')}::${n.qualifiedName} ${String(e.metadata?.resolvedBy)}@${String(e.metadata?.confidence)}`)
.sort();
}
describe('Go standard method names on a receiver nothing types', () => {
it.each([
// `func errorText(err error)`: the error interface's own method.
['errorText', 'a receiver of a predeclared type'],
// `err := doWork()`: an `err` is no `notRegisteredErr`.
['workText', 'a receiver sharing only a word with the owner type'],
// `leaderID := lookupLeader()`: no `MoveLeaderRequest`.
['leaderText', 'a receiver sharing only a word with the owner type'],
['ctxErr', 'a parameter of a type from outside the project'],
// `ctx, cancel := context.WithTimeout(…)`
['timeoutErr', 'a value a package from outside the project hands out'],
// `var mu sync.Mutex`: no project `Mutex`.
['locked', 'a variable of a type from outside the project'],
// `tb testing.TB`, beside a project interface `TB`.
['report', 'a parameter of a type from outside the project'],
// `recorder := httptest.NewRecorder()`
['headers', 'a value a package from outside the project hands out'],
// `status := currentStatus()`, beside a `Status` type and a `RuntimeStatus::String`.
['statusText', 'a receiver named after a type that lacks the method'],
])('links nothing from %s: %s', (fn) => {
expect(calls(fn)).toEqual([]);
});
it.each([
['memberText', 'types/id.go::ID::String instance-method@0.9'],
// Not `StringIter::Err` or `symbolsIter::Err`, by the word "iter".
['iterErr', 'chunks/iterator.go::Iterator::Err instance-method@0.9'],
])('links %s through the type its parameter is declared as', (fn, target) => {
expect(calls(fn)).toEqual([target]);
});
it.each([
// `for _, id := range ids`
['idsText', 'types/id.go::ID::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(…)` 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);
});
it("leaves typed receivers and other method names as they were", () => {
const describeFn = cg.getNodesInFile('server/server.go').find((n) => n.qualifiedName === 'Server::Describe');
expect(describeFn).toBeDefined();
const targets = cg.getOutgoingEdges(describeFn!.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName);
expect(targets).toEqual(['Server::String']);
expect(calls('startSrv').filter((c) => c.includes('Srv::Start'))).toHaveLength(1);
});
});