fix(go): a type in a signature or composite literal links a type, never a method of its name (#2416)

A Go type position -- a parameter or result type, a composite literal's
type -- names a type of the package Go reads it from. Name matching (and
the instance-method guess, and the Express resolver reading Go refs) took
whatever declaration shared the name: etcd's
`func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked both types
to the method `treeIndex.keyIndex`, prometheus's `samples{...}` literals to
`sampleRing.samples`, and `config.URL{...}` from an outside package to
`scrape.Target.URL`.

gateTargetKind now hands every Go type position to goTypePositionTarget:
a method or function target moves to the type of that name in the
reference's own package (bare) or the imported project package (`pkg.T`),
or is dropped when there is none; a bare name that found another
package's type moves to its own package's type when it declares one
(prometheus's `prompb` builds its own `Histogram_CountInt`). A generic
method's receiver (`func (p *Pool[T]) Get() T`) now declares type
parameters for gateTypeParameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-07 09:10:09 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 9d07864e65
commit 3bf6b8f1f5
5 changed files with 310 additions and 3 deletions
+1
View File
@@ -37,6 +37,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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.
- In Go, a type in a parameter, a result or a composite literal now always links to a type, never to a method or function that shares its name. Before, an unexported type like `keyIndex` in etcd's `func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked to the `keyIndex` method declared beside it, prometheus's `samples{…}` literals to a `samples` method, and a type from a package outside your project, like `config.URL` in `&config.URL{…}`, to whatever project method had that name. A type written without a package, including an unexported one or one named like `AppenderV2` or `Histogram_CountInt`, now links to its own package's type when there is one, and a generic method's type parameter, like `T` in `func (p *Pool[T]) Get() T`, no longer links to an unrelated `T`. Re-index Go projects after upgrading.
- Indexing a project that includes large bundled JavaScript files, such as a copy of pdf.js or d3, is fast again: since 1.6.2, resolving the calls in a JavaScript or TypeScript file re-read the file's text above each call, so a single bundled library could add many seconds to an index. The graph it builds is unchanged. Thanks @bompus for the report. (#2334)
- Indexing a project that ships a large minified JavaScript file, such as the GraphiQL bundle inside go-ethereum, no longer stalls at "Resolving refs" when CodeGraph runs on Node 22, and linking such a file is faster on any Node version. The graph it builds is unchanged.
- The time `codegraph init` and `codegraph index` print beside the node and edge counts now covers the whole run, resolving references and linking included, and `codegraph sync` reports its whole run the same way. Before, it counted only reading and parsing the files, which can be a small part of an index, so a slow index looked fast. Thanks @bompus for the report. (#2334)
+245
View File
@@ -0,0 +1,245 @@
/**
* A Go type position — a parameter or result type, a composite literal's
* type — names a type: a struct, an interface or a defined type, of the
* package Go reads the name from. Never a method or a function, which Go
* reaches only through a value or a package. Name matching took whatever
* declaration shared the name: etcd's
* `func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked both
* `keyIndex` types to the method `treeIndex.keyIndex` beside it, prometheus's
* `(ec2Client, error)` result to the method `EC2Discovery.ec2Client` it
* declares, every `samples{…}` literal to `sampleRing.samples`, and
* `&config_util.URL{…}` (an outside package) to `Target.URL`. A generic
* receiver's `[T]` declares `T` too: `func (p *Pool[T]) Get() T` returns the
* pool's own element type, not the method `Sample.T` of another package.
*/
import { describe, it, expect, afterAll, beforeAll } 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-type-pos-'));
const files: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
'mvcc/key_index.go': `package mvcc
type keyIndex struct {
key []byte
}
type revisions []int64
`,
'mvcc/index.go': `package mvcc
type treeIndex struct {
tree map[string]*keyIndex
}
func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex {
return ti.keyIndex(keyi)
}
func (ti *treeIndex) keyIndex(keyi *keyIndex) *keyIndex {
return &keyIndex{key: keyi.key}
}
func (ti *treeIndex) revisions() revisions {
return revisions{1, 2}
}
`,
'mvcc/watchable_store.go': `package mvcc
type contains interface {
contains(rev int64) bool
}
func kvsToEvents(c contains, revs [][]byte) {}
`,
'discovery/aws/ec2.go': `package aws
import "context"
type ec2Client interface {
DescribeInstances(ctx context.Context) error
}
type EC2Discovery struct {
client ec2Client
}
func (d *EC2Discovery) ec2Client(ctx context.Context) (ec2Client, error) {
return d.client, nil
}
`,
'util/zeropool/pool.go': `package zeropool
type Pool[T any] struct {
items []T
}
func (p *Pool[T]) Get() T {
var zero T
return zero
}
func (p *Pool[T]) Put(item T) {}
type LazyLoader[K comparable, V any] struct{}
func (l *LazyLoader[K, V]) Load(key K) (V, error) {
var v V
return v, nil
}
`,
'prompb/types.pb.go': `package prompb
type Histogram struct {
Count isHistogram_Count
}
type isHistogram_Count interface {
isHistogram_Count()
}
type Histogram_CountInt struct {
CountInt uint64
}
func (*Histogram_CountInt) isHistogram_Count() {}
func (m *Histogram) GetCount() isHistogram_Count {
return m.Count
}
`,
'prompb/codec.go': `package prompb
func FromIntHistogram(count uint64) Histogram {
return Histogram{
Count: &Histogram_CountInt{CountInt: count},
}
}
`,
'prompb/io/prometheus/write/v2/types.pb.go': `package writev2
type Histogram_CountInt struct {
CountInt uint64
}
type Sample struct {
ts int64
}
func (s Sample) T() int64 { return s.ts }
`,
'model/value/value.go': `package value
type T struct{}
type K struct{}
`,
'storage/remote/client_test.go': `package remote
import (
"testing"
config_util "github.com/prometheus/common/config"
)
type ClientConfig struct {
URL *config_util.URL
}
func TestStoreHTTPErrorHandling(t *testing.T) {
conf := &ClientConfig{
URL: &config_util.URL{URL: nil},
}
_ = conf
}
`,
'scrape/target.go': `package scrape
type Target struct{}
func (t *Target) URL() string { return "" }
`,
};
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 });
});
/** The nodes of `file`, or the one named `from`. */
const sourcesIn = (file: string, from?: string) => cg.getNodesInFile(file).filter((n) => !from || n.name === from);
/**
* `kind file:qualifiedName` of each type-position edge (a `references` or an
* `instantiates`) out of `file`'s nodes whose target is named `name`,
* deduplicated and sorted.
*/
const typeLinksFrom = (file: string, name: string, from?: string) => {
const links = cg.getOutgoingEdgesFrom(sourcesIn(file, from).map((n) => n.id))
.filter((e) => e.kind === 'references' || e.kind === 'instantiates')
.map((e) => ({ kind: e.kind, target: cg.getNode(e.target)! }))
.filter(({ target }) => target.name === name)
.map(({ kind, target }) => `${kind} ${target.filePath}:${target.qualifiedName}`);
return [...new Set(links)].sort();
};
describe('A Go type position resolves to a type', () => {
it('an unexported parameter and result type is its package’s struct, not a method of that name', () => {
expect(typeLinksFrom('mvcc/index.go', 'keyIndex', 'KeyIndex')).toEqual(['references mvcc/key_index.go:keyIndex']);
expect(typeLinksFrom('mvcc/index.go', 'keyIndex', 'keyIndex')).toEqual([
'instantiates mvcc/key_index.go:keyIndex',
'references mvcc/key_index.go:keyIndex',
]);
});
it('the method a call names stays a method', () => {
const calls = cg.getOutgoingEdgesFrom(sourcesIn('mvcc/index.go', 'KeyIndex').map((n) => n.id))
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName);
expect(calls).toEqual(['treeIndex::keyIndex']);
});
it('a defined type in a result and a composite literal is that type', () => {
expect(typeLinksFrom('mvcc/index.go', 'revisions', 'revisions')).toEqual([
'instantiates mvcc/key_index.go:revisions',
'references mvcc/key_index.go:revisions',
]);
});
it('an interface whose method shares its name is the interface', () => {
expect(typeLinksFrom('mvcc/watchable_store.go', 'contains', 'kvsToEvents')).toEqual(['references mvcc/watchable_store.go:contains']);
expect(typeLinksFrom('prompb/types.pb.go', 'isHistogram_Count', 'GetCount')).toEqual(['references prompb/types.pb.go:isHistogram_Count']);
});
it('a result type is the interface, not the method the line declares', () => {
expect(typeLinksFrom('discovery/aws/ec2.go', 'ec2Client', 'ec2Client')).toEqual(['references discovery/aws/ec2.go:ec2Client']);
});
it('a bare composite literal is its own package’s struct, not a namesake in another package', () => {
expect(typeLinksFrom('prompb/codec.go', 'Histogram_CountInt')).toEqual(['instantiates prompb/types.pb.go:Histogram_CountInt']);
});
it('a generic receiver’s type parameters are type parameters', () => {
expect(typeLinksFrom('util/zeropool/pool.go', 'T', 'Get')).toEqual([]);
expect(typeLinksFrom('util/zeropool/pool.go', 'T', 'Put')).toEqual([]);
expect(typeLinksFrom('util/zeropool/pool.go', 'K', 'Load')).toEqual([]);
expect(typeLinksFrom('util/zeropool/pool.go', 'V', 'Load')).toEqual([]);
});
it('a composite literal of an outside package’s type is no project method of that name', () => {
expect(typeLinksFrom('storage/remote/client_test.go', 'URL')).toEqual([]);
});
});
+13 -1
View File
@@ -21,7 +21,7 @@ import {
isImportableKind,
CPP_DEFINE_SIGNATURE,
} from './types';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoBareName, GO_TYPE_KINDS } from './name-matcher';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
@@ -3091,6 +3091,10 @@ export class ReferenceResolver {
* inside a type never is, and neither is the import statement the reference
* was written in.
*
* For a Go type position: the target is a type of the package Go reads the
* name from, moved there when a strategy found a method, a function or
* another package's type (see goTypePositionTarget).
*
* For `extends`/`implements`, it cannot be describing a real supertype when:
*
* 1. The target's kind can never be a supertype (an enum member, a method,
@@ -3142,6 +3146,14 @@ export class ReferenceResolver {
return target && !isImportableKind(target.kind) ? null : result;
}
// A Go type position — a parameter or result type, a composite literal's
// type — names a type of the package Go reads it from (route handlers are
// `references` too, but values).
if (ref.language === 'go' && (ref.referenceKind === 'instantiates' ||
(ref.referenceKind === 'references' && this.nodeById(ref.fromNodeId)?.kind !== 'route'))) {
return goTypePositionTarget(result, ref, this.context);
}
if (!isInheritanceRef(ref)) return result;
const target = this.nodeById(result.targetNodeId);
if (target && !isSupertypeTarget(target)) {
+42
View File
@@ -1970,6 +1970,48 @@ export function isGoUnknownQualified(ref: UnresolvedRef, context: ResolutionCont
return written !== undefined && imported === undefined;
}
/**
* What a Go type position — a parameter or result type, a composite
* literal's type — names: a type, which Go reads from one package. A method
* or a function is never it; Go reaches those only through a value or a
* package. Whichever strategy found a declaration of the name, the type is
* the one of that name in the reference's own package for a bare name, or in
* the imported project package for `pkg.T`. Without one there, a method or
* function of the name is nothing the reference means. etcd's
* `func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked both
* `keyIndex` types to the method `treeIndex.keyIndex` beside it,
* prometheus's `(ec2Client, error)` result to the method the line declares,
* and its `&config_util.URL{…}` (an outside package) to `Target.URL`. A bare
* name that found another package's type means its own package's type of
* that name when there is one: prometheus's `prompb` builds its own
* `Histogram_CountInt`, not the `write/v2` one.
*/
export function goTypePositionTarget(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const target = context.getNodeById?.(result.targetNodeId);
if (!target || target.language !== 'go') return result;
const isType = GO_TYPE_KINDS.has(target.kind);
// A composite literal keeps its package in the name (`config_util.URL`);
// a parameter type leaves it on the line.
const dot = ref.referenceName.lastIndexOf('.');
const name = ref.referenceName.slice(dot + 1);
// The package's directory; null for a package outside the project,
// undefined for a qualifier that is none of the file's imports as indexed.
let pkgDir: string | null | undefined;
let bare = false;
if (dot >= 0) {
pkgDir = goImportPackageDir(ref.referenceName.slice(0, dot), ref.filePath, context);
} else {
const { written, imported } = goRefQualification(ref, context);
bare = written === undefined;
if (bare) pkgDir = goPackageDir(ref.filePath);
else if (imported) pkgDir = context.getGoPackageDir?.(imported.source, ref.filePath) ?? null;
}
if (isType && (!bare || goPackageDir(target.filePath) === pkgDir)) return result;
const types = pkgDir ? goPackageTypes(name, pkgDir, context) : [];
if (types.length > 0) return { ...result, targetNodeId: preferCallSiteFile(types, ref.filePath)[0]!.id };
return isType ? result : null;
}
/**
* Whether a Go reference is written through an imported package from outside
* the project's modules — `context.Context`, `fmt.Errorf`, a third-party
+9 -2
View File
@@ -9,8 +9,9 @@
* from the declaration heads around the reference: after the declared name
* (`class Foo<T>`, `fn f<T: Display>`, `func F[T any]`, `def f[F[_]: Monad, A]`,
* `struct Stack<Element>`), before it where the language puts them
* (Java's `<T extends X> T max(…)`, Kotlin's `fun <T> f()`), and a C++
* `template <typename T>` line above it. Applied at resolveOne's seam, so every
* (Java's `<T extends X> T max(…)`, Kotlin's `fun <T> f()`), a C++
* `template <typename T>` line above it, and a Go method's receiver
* (`func (p *Pool[T]) Get() T`). Applied at resolveOne's seam, so every
* strategy's result obeys it.
*/
import type { Node } from '../types';
@@ -152,6 +153,12 @@ function declaredTypeParameters(node: Node, context: ResolutionContext): Readonl
if (before[i] === '<' && (i === 0 || /[\s(,]/.test(before[i - 1]!))) collect(before, i, names);
}
}
// Go: a method of a generic type declares the type's parameters again in
// its receiver — `func (p *Pool[T]) Get() T`, `func (LazyLoader[K, V]) …`.
if (node.language === 'go' && node.kind === 'method') {
const receiver = /^\s*func\s*\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*[A-Za-z_]\w*\s*\[/.exec(head);
if (receiver) collect(head, receiver[0].length - 1, names);
}
// C++: `template <typename T, class U>` on the lines just above.
if (node.language === 'cpp') {
const above = lines.slice(Math.max(0, node.startLine - 3), node.startLine).join('\n');