Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
245 changes: 245 additions & 0 deletions __tests__/go-type-position-kinds.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
14 changes: 13 additions & 1 deletion src/resolution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down
42 changes: 42 additions & 0 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/resolution/type-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down