fix(go): a method a type declares in a file the index couldn't read is not its embedded type's (#2757)

kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB
limit, so every API type's own String, Marshal, Unmarshal, Size and
MarshalToSizedBuffer is missing from the graph. Resolving `pod.String()` on
a `*v1.Pod`, or `m.Template.Size()` on a `v1.PodTemplateSpec`, the walk over
embedded types found the method the embedded TypeMeta or ObjectMeta has and
linked that, a method the call never runs.

resolveGoMethodInPackage, which every typed Go receiver goes through
(parameters, locals, fields, assertions, package-level values, indexed
elements), now reads the method declarations of the files of the type's
package that the index couldn't read, from their text, and links nothing
when the type declares the method there. matchMethodCall's typed branch and
matchGoPackageValueCall keep that null instead of falling through to name
matching, which found the same promoted method. A method the type really
gets from an embedded type, like GetName from ObjectMeta, still links. The
check that #2755 added for indexed elements moves into the shared path.

On kubernetes 4c05e667, 63 calls lose that link and nothing else changes;
etcd, prometheus, express and flask are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 17:44:33 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a5fe6e1a27
commit 1f3a274c53
3 changed files with 187 additions and 19 deletions
+1
View File
@@ -46,6 +46,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Go, a call made through a type assertion, like `srv.(KVServer).Range(ctx, in)` in generated gRPC code, `c.Reader.(*pipe).Close()` or `v.(storage.Store).Fetch(key)`, now links to that method of the asserted type, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, it linked to whichever method shared the name, so every gRPC handler pointed at the `Unimplemented…Server` stub instead of the server interface. Re-index Go projects after upgrading.
- 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 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)
- In Go, an exported type written without a package, like `Node` in `func Walk(v Visitor, node Node)`, now links to the type of that name in its own package, whether it is a struct, an interface or another kind of type. Before, it could link to a struct of the same name in an unrelated package, so prometheus's PromQL parser functions pointed at the Kubernetes discovery `Node` struct instead of the parser's own `Node` interface, or, for a result type like `Appender` in `func (f *fanout) Appender(…) Appender`, to the method itself. A type from a package outside your project, like `apiv1.Node` or `http.Handler`, no longer links to a struct of the same name in the file that uses it; a variadic parameter like `...storage.Filter` now links to that package's type; a route handler written as a method value, like `h.Follow` in an Echo or Gin app, links to that method rather than a model struct named `Follow`; and a method called on another call's result or on an indexed value, like `err[i].Error()` or a chained `.String()`, is no longer recorded as creating a struct of the same name. Re-index Go projects after upgrading.
- In Go, a function or method passed as a value, like `sync.Pool{New: wm.new}`, now shows the function that passes it among its callers and impact even when it is named `new`, `nil`, `None`, `self` or a handful of similar words, or when a line break or comment sits beside the dot. Before, such a value was skipped, so a method used only that way looked unused. Re-index Go projects after upgrading.
+150
View File
@@ -0,0 +1,150 @@
/**
* A Go method a type declares in a file the index couldn't read.
*
* kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB
* limit, so `Pod`'s own `String`, `Marshal` and `Unmarshal` are not in the
* graph. Resolving `pod.String()` on a `*v1.Pod`, the walk over the types
* `Pod` embeds found the `String` its `TypeMeta` has and linked that, a
* method the call never runs. Every typed Go receiver took that walk: a
* parameter, a local, a field, an assertion, a package-level variable.
*
* The method declarations of such a file are read from its text: when the
* type declares the method there, the call links nothing. A method the type
* really gets from an embedded type, like `GetName` from `ObjectMeta`, still
* links.
*/
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-unread-methods-'));
const files: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
'meta/meta.go': `package meta
type TypeMeta struct {
Kind string
}
func (m *TypeMeta) String() string { return m.Kind }
func (m *TypeMeta) Unmarshal(dAtA []byte) error { return nil }
type ObjectMeta struct {
Name string
}
func (m *ObjectMeta) GetName() string { return m.Name }
`,
'api/types.go': `package api
import "example.com/app/meta"
type Pod struct {
meta.TypeMeta
meta.ObjectMeta
Spec string
}
`,
// Over the index's 1 MB limit, so none of it is read.
'api/generated.pb.go': `package api
func (m *Pod) String() string { return "" }
func (m *Pod) Unmarshal(dAtA []byte) error { return nil }
${'// generated padding, never read by the indexer\n'.repeat(24_000)}`,
'caller/caller.go': `package caller
import "example.com/app/api"
func param(pod *api.Pod) string {
return pod.String()
}
func local() error {
pod := &api.Pod{}
return pod.Unmarshal(nil)
}
type holder struct {
pod *api.Pod
}
func (h *holder) field() string {
return h.pod.String()
}
func asserted(x any) string {
return x.(*api.Pod).String()
}
var shared api.Pod
func packageVar() error {
return shared.Unmarshal(nil)
}
func promoted(pod *api.Pod) string {
return pod.GetName()
}
`,
};
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 });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `file::qualifiedName` of every symbol the function or method `name` in `file` calls. */
function callTargets(file: string, name: string): string[] {
const fn = cg.getNodesInFile(file).find((n) => n.name === name);
expect(fn, `${name} in ${file}`).toBeDefined();
return cg
.getOutgoingEdges(fn!.id)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.filePath.replace(/\\/g, '/')}::${n.qualifiedName}`)
.sort();
}
describe("Go methods declared in a file the index couldn't read", () => {
it('the file is indexed without a node', () => {
expect(cg.getNodesInFile('api/generated.pb.go')).toEqual([]);
});
it('a parameter of the type links nothing, not the method an embedded type has', () => {
expect(callTargets('caller/caller.go', 'param')).toEqual([]);
});
it('a local bound to a composite literal', () => {
expect(callTargets('caller/caller.go', 'local')).toEqual([]);
});
it('a field of the type', () => {
expect(callTargets('caller/caller.go', 'field')).toEqual([]);
});
it('a type assertion', () => {
expect(callTargets('caller/caller.go', 'asserted')).toEqual([]);
});
it('a package-level variable', () => {
expect(callTargets('caller/caller.go', 'packageVar')).toEqual([]);
});
it('a method an embedded type gives the type still links', () => {
expect(callTargets('caller/caller.go', 'promoted')).toEqual(['meta/meta.go::ObjectMeta::GetName']);
});
});
+36 -19
View File
@@ -19275,6 +19275,10 @@ export function matchMethodCall(
// defined over an interface (`type Codec Serializer`) has methods the
// index doesn't give it.
if (goStd && goDeclared !== undefined) 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.
if (ref.language === 'go' && importedFqn !== undefined && goUnreadFileDeclares(inferredType, methodName!, importedFqn, context)) 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
@@ -19931,8 +19935,10 @@ export const GO_TYPE_KINDS: ReadonlySet<string> = new Set(['struct', 'interface'
* method the type doesn't declare is promoted from a type it embeds, which
* its declaration names (`BaseAPI`, `*cached.BaseManager`, `ReadView`); the
* name-based supertype walk would mix in every same-named type's embeddings.
* Undefined when the package declares no such type (a dot import, a file
* that isn't indexed): the caller then resolves by name as before.
* One the type declares in a file of its package the index couldn't read is
* missing from the graph, and nothing stands for it. Undefined when the
* package declares no such type (a dot import, a file that isn't indexed):
* the caller then resolves by name as before.
*/
function resolveGoMethodInPackage(
typeName: string,
@@ -19952,6 +19958,10 @@ function resolveGoMethodInPackage(
}
const types = goPackageTypes(typeName, pkgDir, context);
if (types.length === 0) return undefined;
// kubernetes' core/v1 generated.pb.go is too large to index, so `Pod`'s
// own `String` is missing, and `pod.String()` went to the one its embedded
// `TypeMeta` has.
if (goUnreadFileDeclares(typeName, methodName, pkgDir, context)) return null;
if (depth >= 4) return null;
let unplaced = 0;
for (const t of types) {
@@ -20328,8 +20338,10 @@ function matchGoPackageValueCall(receiver: string, method: string, ref: Unresolv
// A project type whose methods aren't found is left to the strategies
// below: kubernetes' `var UnstructuredJSONScheme runtime.Codec =
// unstructuredJSONScheme{}` is a `Codec`, defined over an interface whose
// methods its node doesn't carry.
return resolveMethodOnType(name, method, ref, context, 0.9, 'instance-method', pkgDir) ?? undefined;
// methods its node doesn't carry. Not when the type declares the method in
// a file the index couldn't read: that one is the type's own.
return resolveMethodOnType(name, method, ref, context, 0.9, 'instance-method', pkgDir) ??
(goUnreadFileDeclares(name, method, pkgDir, context) ? null : undefined);
}
/** A Go package-level value's type as its declaration writes it, with the declaring file; null for one with no project method. */
@@ -20697,20 +20709,24 @@ function goMethodOnElement(t: GoTypeText, ref: UnresolvedRef, context: Resolutio
// An alias of a type from outside the project (`type Ctx = context.Context`)
// has that type's methods, none the project declares.
if (named.nodes.every((n) => goAliasTarget(n, context) === null)) return null;
const result = resolveMethodOnType(named.name, ref.referenceName, ref, context, 0.9, 'instance-method', named.dir);
// A type's own method may sit in a file of its package the index couldn't
// read: kubernetes' core/v1 generated.pb.go is too large to index, so
// `PersistentVolumeClaim`'s own `Unmarshal` is missing, and the one its
// embedded `TypeMeta` has would take the call. What such a file declares
// is read from its text; the call then links nothing.
const target = result ? context.getNodeById?.(result.targetNodeId) : null;
if (!target || goPackageDir(target.filePath) !== named.dir || target.qualifiedName !== `${named.name}::${target.name}`) {
for (const f of goUnreadPackageFiles(named.dir, context)) {
const methods = goUnreadFileMethods(f, context);
if (methods === null || methods.has(`${named.name}::${ref.referenceName}`)) return null;
}
return resolveMethodOnType(named.name, ref.referenceName, ref, context, 0.9, 'instance-method', named.dir);
}
/**
* 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
* method is then missing from the graph, and the one an embedded type has
* is not it. kubernetes' 1.7 MB core/v1 generated.pb.go holds `Pod`'s and
* `PersistentVolumeClaim`'s own `String`, `Marshal` and `Unmarshal`. Read
* from the files' text, so a method that really is promoted, like the
* `GetName` `ObjectMeta` gives `Pod`, still links.
*/
function goUnreadFileDeclares(typeName: string, methodName: string, dir: string, context: ResolutionContext): boolean {
for (const f of goUnreadPackageFiles(dir, context)) {
const methods = goUnreadFileMethods(f, context);
if (methods === null || methods.has(`${typeName}::${methodName}`)) return true;
}
return result;
return false;
}
const GO_PACKAGE_FILES = new WeakMap<ResolutionContext, Map<string, string[]>>();
@@ -20740,8 +20756,9 @@ function goUnreadPackageFiles(dir: string, context: ResolutionContext): string[]
}
GO_PACKAGE_FILES.set(context, files);
}
// A file the index read has a node of its own at least.
const unread = (files.get(dir) ?? []).filter((f) => context.getNodesInFile(f).length === 0);
// A file the index read has a node of its own, named after it.
const unread = (files.get(dir) ?? []).filter((f) =>
(context.getNodesInFileNamed?.(f, path.posix.basename(f)) ?? context.getNodesInFile(f)).length === 0);
memo.set(dir, unread);
return unread;
}