mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
fix(go): a field typed by a dot-imported package resolves in that package (#2764)
A Go type written without a package qualifier was looked up only in the
file's own package. In a file that dot-imports a project package, like
harbor's config tests (`import . ".../controller/config"`, then a
`controller Controller` field), the type isn't there, so the call
`c.controller.AllConfigs()` fell back to name matching. Name matching
took the same-named type of a package the file doesn't import, the one
that sorts first: matchGoFieldChainCall used the struct's own directory
for a bare field type, resolveGoMethodInPackage found no such type there,
and resolveMethodOnType picked the first of every
`Controller::AllConfigs`.
The same gap affected calls through parameters, locals and package-level
variables of such a type (one set by a conversion), through an alias of
it, and through methods promoted from an embedded field. It also affected
method values, type references, and the embedding edge itself where the
namesake was as near.
The field chain, the typed-receiver and method-value package lookup,
embedded types, alias targets, package-level values and Go type positions
now ask goBareTypeDir. It reads a bare name as Go does: the file's own
package first, then a project package the file dot-imports that declares
it. An exported name that only a dot import of a package outside the
project's modules can supply (`. "github.com/onsi/gomega"`) names none
of the project's types. The exception is a type that one of the file's
functions declares, which the index holds no node for: name matching
still handles it, as before. A bare embedding is retargeted only when a
dot import decides, and files without dot imports resolve exactly as
before.
Tests: go-dot-import-field-type.test.ts has 10 cases on both the kernel
and wasm backends. 18 tests fail on main and 2 guards pass. All
go-*, goframe, gin-*, frameworks*, resolution*, sync* and dead-code test
files pass (65 files, 1,140 tests). tsc and the build are clean.
Validation compares main bf6f1fa6 with this commit, each built with a
kernel from its own sources. Each repo was indexed from scratch and
diffed by call site:
- gomega 820ad4d5: 30 calls retargeted from internal.Gomega's Expect to
types.Gomega's, the type the dot-imported root package's `Gomega`
alias names. All 30 were checked against source.
- ginkgo 7ff96d7d: 135 calls added and 1,022 confirmed (same target,
0.7 to 0.9), all through package-level vars typed by the dot-imported
internal/test_helpers. 9 retargeted (8 composite literals to the root
package's SemVerConstraints aliases, 1 Status call to the fake
handler). None removed.
- harbor src/ 4e07c69c and kubernetes d2c4b85b: byte-identical dumps.
Their dot-imported types' methods have no namesakes, so main already
linked them.
- Controls gin 0f09c3a9 and zustand d7a5583c (TypeScript): byte-identical.
- Index time and DB size are within noise. On kubernetes, the median of
3 interleaved rounds was 151.5 s on main and 148.7 s with the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bf6f1fa67e
commit
efff745783
@@ -44,6 +44,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Dart, a type is now linked from more of the places it is written, not only from parameters and return types: the type an `extension … on` targets, a field's type, a top-level variable's type, generic arguments such as `Future<Report?>.value(null)` or Riverpod's `final reportProvider = Family<Report?, String>()`, and a local variable's type, a cast or a type check inside a function. So `callers` and impact for a model class now reach the extensions, models and providers that use it. Thanks @mg-mg-mg for the report. (#2327)
|
||||
- In Dart, a `typedef` now links to the types it is built from, like `Report` in `typedef ReportCallback = void Function(Report r);` or `typedef Reports = List<Report>;`, so `callers` and impact for a model class also reach the callback and collection types defined on it. An old-style typedef with a return type, like `typedef Widget ItemBuilder(BuildContext context);`, is now indexed under its own name instead of its return type's, so code that only uses `Widget` no longer links to it. Thanks @mg-mg-mg for the report. (#2327)
|
||||
- In Go, calls through a method receiver or parameter of an unexported type, the usual shape of gRPC and HTTP handlers (`s.service.AddItem()` inside `func (s *server) Create()`), now resolve, and always within that type's own package: another package's `server` with a same-named method no longer takes the call, and a method the type gets from a struct or interface it embeds is found too. Re-index Go projects after upgrading. Thanks @GoDiao for the report and the fix. (#2323)
|
||||
- In Go, a type a file gets from a dot import, like `Controller` in harbor's config tests after `import . "…/controller/config"`, is now looked up in that package, so `c.controller.AllConfigs()` on a `controller Controller` field, a call through a parameter, variable, alias or embedded field of that type, and the type's own links reach that package's `Controller` instead of a same-named type in a package the file doesn't import. A name the file can only get from a dot import of a package outside your project, like `. "github.com/onsi/gomega"`, no longer links to a project type that shares it. Re-index Go projects after upgrading. Thanks @GoDiao. (#2323)
|
||||
- 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.
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* A Go type written without a package qualifier is the file's own package's,
|
||||
* or one of a package the file dot-imports (#2323):
|
||||
*
|
||||
* import . "example.com/app/config"
|
||||
*
|
||||
* type controllerSuite struct {
|
||||
* controller Controller // config.Controller
|
||||
* }
|
||||
*
|
||||
* func (c *controllerSuite) GetAll() {
|
||||
* c.controller.AllConfigs() // config.Controller's, not aaa.Controller's
|
||||
* }
|
||||
*
|
||||
* The type was looked up in the file's own package alone. Not found there,
|
||||
* the call went to name matching, which took the same-named type of a
|
||||
* package the file doesn't import — the one that sorts first. harbor's config
|
||||
* tests have this shape. The same held for a parameter, a local or a
|
||||
* package-level variable of such a type, an alias of it, a method promoted
|
||||
* from an embedded one and the embedding itself. A type of the file's own
|
||||
* package of the name comes first; a name the file can only have from a
|
||||
* package outside the project's modules (`. "github.com/onsi/gomega"`) has
|
||||
* none of the project's methods, unless a function of the file declares a
|
||||
* type of that name, which hides the dot import's.
|
||||
*/
|
||||
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/index';
|
||||
|
||||
/**
|
||||
* The module the tests index. `aaa` declares a namesake of every type and
|
||||
* sorts ahead of `config`, where name matching looked first.
|
||||
*/
|
||||
const FILES: Record<string, string> = {
|
||||
'go.mod': 'module example.com/app\n\ngo 1.22\n',
|
||||
'aaa/aaa.go': `package aaa
|
||||
|
||||
type Controller interface {
|
||||
AllConfigs() map[string]any
|
||||
UserConfigs() map[string]any
|
||||
}
|
||||
|
||||
type Manager struct{}
|
||||
|
||||
func (m *Manager) Save() error { return nil }
|
||||
|
||||
type Level int
|
||||
|
||||
func (l Level) Label() string { return "" }
|
||||
|
||||
type Gomega interface {
|
||||
Expect(actual any) bool
|
||||
}
|
||||
`,
|
||||
'config/controller.go': `package config
|
||||
|
||||
type Controller interface {
|
||||
AllConfigs() map[string]any
|
||||
UserConfigs() map[string]any
|
||||
}
|
||||
|
||||
type Manager struct{}
|
||||
|
||||
func (m *Manager) Save() error { return nil }
|
||||
|
||||
type Level int
|
||||
|
||||
func (l Level) Label() string { return "" }
|
||||
`,
|
||||
// The issue's file, and the other ways a file can use a dot-imported type.
|
||||
'config/test/suite.go': `package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "example.com/app/config"
|
||||
)
|
||||
|
||||
type controllerSuite struct {
|
||||
t *testing.T
|
||||
controller Controller
|
||||
mgr *Manager
|
||||
}
|
||||
|
||||
func (c *controllerSuite) GetAll() {
|
||||
c.controller.AllConfigs()
|
||||
}
|
||||
|
||||
func (c *controllerSuite) Persist() error {
|
||||
return c.mgr.Save()
|
||||
}
|
||||
|
||||
func ViaParam(s *controllerSuite) {
|
||||
s.controller.UserConfigs()
|
||||
}
|
||||
|
||||
func Param(ctl Controller) {
|
||||
ctl.AllConfigs()
|
||||
}
|
||||
|
||||
func Local() {
|
||||
var ctl Controller
|
||||
ctl.UserConfigs()
|
||||
var m *Manager
|
||||
m.Save()
|
||||
}
|
||||
|
||||
func Indexed(ctls []Controller) {
|
||||
ctls[0].AllConfigs()
|
||||
}
|
||||
|
||||
type embedded struct {
|
||||
Controller
|
||||
*Manager
|
||||
}
|
||||
|
||||
func (e *embedded) Run() {
|
||||
e.AllConfigs()
|
||||
e.Save()
|
||||
}
|
||||
|
||||
var shared Controller
|
||||
|
||||
var level = Level(2)
|
||||
|
||||
func Shared() string {
|
||||
shared.AllConfigs()
|
||||
return level.Label()
|
||||
}
|
||||
|
||||
type Ctl = Controller
|
||||
|
||||
func Aliased(c Ctl) {
|
||||
c.UserConfigs()
|
||||
}
|
||||
|
||||
func register(fn any) {}
|
||||
|
||||
func Values(ctl Controller, c *controllerSuite) {
|
||||
register(ctl.AllConfigs)
|
||||
register(c.controller.UserConfigs)
|
||||
}
|
||||
`,
|
||||
// A file whose own package declares the name too: Go rejects the clash,
|
||||
// but the package's own type is the one the index reads first. Its
|
||||
// directory is as far from aaa as from config.
|
||||
'own/own.go': `package own
|
||||
|
||||
import . "example.com/app/config"
|
||||
|
||||
type Controller interface {
|
||||
AllConfigs() map[string]any
|
||||
}
|
||||
|
||||
type suite struct {
|
||||
controller Controller
|
||||
}
|
||||
|
||||
func (s *suite) Get() {
|
||||
s.controller.AllConfigs()
|
||||
}
|
||||
|
||||
func Param(ctl Controller) {
|
||||
ctl.AllConfigs()
|
||||
}
|
||||
|
||||
func Uses(m *Manager) error {
|
||||
return m.Save()
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
*Manager
|
||||
}
|
||||
|
||||
func (w *wrapper) Keep() error {
|
||||
return w.Save()
|
||||
}
|
||||
`,
|
||||
// A dot import of a package from outside the project's modules: a name
|
||||
// its own package doesn't declare may be that package's.
|
||||
'ext/ext.go': `package ext
|
||||
|
||||
import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type matcherSuite struct {
|
||||
g Gomega
|
||||
}
|
||||
|
||||
func (m *matcherSuite) Check() {
|
||||
m.g.Expect(1)
|
||||
}
|
||||
|
||||
func Param(g Gomega) {
|
||||
g.Expect(2)
|
||||
}
|
||||
|
||||
func Many(gs []Gomega) {
|
||||
gs[0].Expect(3)
|
||||
}
|
||||
`,
|
||||
// ginkgo's spec context test: a type a function declares hides the dot
|
||||
// import's names, and the index holds no node for it.
|
||||
'ext/local.go': `package ext
|
||||
|
||||
import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type progressManager struct{}
|
||||
|
||||
func (p *progressManager) QueryProgressReporters() []string { return nil }
|
||||
|
||||
func Attached(v any) {
|
||||
type CompleteSpecContext interface {
|
||||
QueryProgressReporters() []string
|
||||
}
|
||||
ctx := v.(CompleteSpecContext)
|
||||
ctx.QueryProgressReporters()
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
describe.each(['default', 'wasm'])('Go types written bare are read in the packages the file dot-imports too (%s)', (backend) => {
|
||||
let root = '';
|
||||
let cg: CodeGraph | undefined;
|
||||
let kernel: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
kernel = process.env.CODEGRAPH_KERNEL;
|
||||
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
|
||||
else delete process.env.CODEGRAPH_KERNEL;
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-dot-import-'));
|
||||
for (const [rel, content] of Object.entries(FILES)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
// One file with a Windows checkout's line endings.
|
||||
fs.writeFileSync(path.join(root, rel), rel === 'own/own.go' ? content.replace(/\n/g, '\r\n') : content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
}, 60_000);
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
|
||||
else process.env.CODEGRAPH_KERNEL = kernel;
|
||||
});
|
||||
|
||||
/** The node `qualifiedName` in `file`. */
|
||||
function nodeOf(file: string, qualifiedName: string) {
|
||||
const node = cg!.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName && n.kind !== 'parameter');
|
||||
expect(node, `${qualifiedName} in ${file}`).toBeDefined();
|
||||
return node!;
|
||||
}
|
||||
|
||||
/**
|
||||
* `<source line> -> <target file>::<target qualified name>` for every edge
|
||||
* of `kind` the function or method `name` in `file` has to a function or
|
||||
* method.
|
||||
*/
|
||||
function links(file: string, name: string, kind: 'calls' | 'references' = 'calls'): string[] {
|
||||
const lines = FILES[file]!.split('\n');
|
||||
return cg!
|
||||
.getOutgoingEdges(nodeOf(file, name).id)
|
||||
.filter((e) => e.kind === kind)
|
||||
.map((e) => ({ e, target: cg!.getNode(e.target)! }))
|
||||
.filter(({ target }) => target.kind === 'method' || target.kind === 'function')
|
||||
.map(({ e, target }) => `${lines[e.line! - 1]!.trim()} -> ${target.filePath}::${target.qualifiedName}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** `<kind> <target file>::<target qualified name>` for each edge of `kinds` from `name` in `file` to a type, as resolved (no synthesized edge). */
|
||||
function typeLinks(file: string, name: string, kinds: string[]): string[] {
|
||||
return cg!
|
||||
.getOutgoingEdges(nodeOf(file, name).id)
|
||||
.filter((e) => kinds.includes(e.kind) && e.provenance !== 'heuristic')
|
||||
.map((e) => ({ e, target: cg!.getNode(e.target)! }))
|
||||
.filter(({ target }) => ['interface', 'struct', 'type_alias'].includes(target.kind))
|
||||
.map(({ e, target }) => `${e.kind} ${target.filePath}::${target.qualifiedName}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
it("resolves a call through a field typed by a dot-imported package's type", () => {
|
||||
// The issue: name matching took aaa's Controller, which sorts first.
|
||||
expect(links('config/test/suite.go', 'controllerSuite::GetAll')).toEqual([
|
||||
'c.controller.AllConfigs() -> config/controller.go::Controller::AllConfigs',
|
||||
]);
|
||||
expect(links('config/test/suite.go', 'controllerSuite::Persist')).toEqual([
|
||||
'return c.mgr.Save() -> config/controller.go::Manager::Save',
|
||||
]);
|
||||
expect(links('config/test/suite.go', 'ViaParam')).toEqual([
|
||||
's.controller.UserConfigs() -> config/controller.go::Controller::UserConfigs',
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves a call through a parameter, a local or a package-level variable of such a type', () => {
|
||||
expect(links('config/test/suite.go', 'Param')).toEqual([
|
||||
'ctl.AllConfigs() -> config/controller.go::Controller::AllConfigs',
|
||||
]);
|
||||
expect(links('config/test/suite.go', 'Local')).toEqual([
|
||||
'ctl.UserConfigs() -> config/controller.go::Controller::UserConfigs',
|
||||
'm.Save() -> config/controller.go::Manager::Save',
|
||||
]);
|
||||
expect(links('config/test/suite.go', 'Indexed')).toEqual([
|
||||
'ctls[0].AllConfigs() -> config/controller.go::Controller::AllConfigs',
|
||||
]);
|
||||
// `var level = Level(2)` converts to the dot-imported type.
|
||||
expect(links('config/test/suite.go', 'Shared')).toEqual([
|
||||
'return level.Label() -> config/controller.go::Level::Label',
|
||||
'shared.AllConfigs() -> config/controller.go::Controller::AllConfigs',
|
||||
]);
|
||||
});
|
||||
|
||||
it('follows an alias of such a type to it', () => {
|
||||
expect(links('config/test/suite.go', 'Aliased')).toEqual([
|
||||
'c.UserConfigs() -> config/controller.go::Controller::UserConfigs',
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves a method promoted from an embedded field of such a type, and links the embedding to it', () => {
|
||||
expect(links('config/test/suite.go', 'embedded::Run')).toEqual([
|
||||
'e.AllConfigs() -> config/controller.go::Controller::AllConfigs',
|
||||
'e.Save() -> config/controller.go::Manager::Save',
|
||||
]);
|
||||
expect(links('own/own.go', 'wrapper::Keep')).toEqual(['return w.Save() -> config/controller.go::Manager::Save']);
|
||||
expect(typeLinks('config/test/suite.go', 'embedded', ['extends', 'implements'])).toEqual([
|
||||
'extends config/controller.go::Manager',
|
||||
'implements config/controller.go::Controller',
|
||||
]);
|
||||
expect(typeLinks('own/own.go', 'wrapper', ['extends', 'implements'])).toEqual(['extends config/controller.go::Manager']);
|
||||
});
|
||||
|
||||
it('links a method value taken through such a receiver as the call would', () => {
|
||||
expect(links('config/test/suite.go', 'Values', 'references')).toEqual([
|
||||
'register(c.controller.UserConfigs) -> config/controller.go::Controller::UserConfigs',
|
||||
'register(ctl.AllConfigs) -> config/controller.go::Controller::AllConfigs',
|
||||
]);
|
||||
});
|
||||
|
||||
it("links a type written bare to the dot-imported package's type", () => {
|
||||
// A struct's field types, a parameter's type: the dot import's even
|
||||
// where another package's namesake is as near.
|
||||
expect(typeLinks('config/test/suite.go', 'controllerSuite', ['references'])).toEqual([
|
||||
'references config/controller.go::Controller',
|
||||
'references config/controller.go::Manager',
|
||||
]);
|
||||
expect(typeLinks('config/test/suite.go', 'Param', ['references'])).toEqual(['references config/controller.go::Controller']);
|
||||
expect(typeLinks('own/own.go', 'Uses', ['references'])).toEqual(['references config/controller.go::Manager']);
|
||||
});
|
||||
|
||||
it("reads the file's own package's type of the name first", () => {
|
||||
expect(links('own/own.go', 'suite::Get')).toEqual(['s.controller.AllConfigs() -> own/own.go::Controller::AllConfigs']);
|
||||
expect(links('own/own.go', 'Param')).toEqual(['ctl.AllConfigs() -> own/own.go::Controller::AllConfigs']);
|
||||
expect(typeLinks('own/own.go', 'suite', ['references'])).toEqual(['references own/own.go::Controller']);
|
||||
// A name its own package lacks is still the dot import's.
|
||||
expect(links('own/own.go', 'Uses')).toEqual(['return m.Save() -> config/controller.go::Manager::Save']);
|
||||
});
|
||||
|
||||
it('makes no guess onto a project type through a dot import from outside the project', () => {
|
||||
expect(links('ext/ext.go', 'matcherSuite::Check')).toEqual([]);
|
||||
expect(links('ext/ext.go', 'Param')).toEqual([]);
|
||||
expect(links('ext/ext.go', 'Many')).toEqual([]);
|
||||
expect(typeLinks('ext/ext.go', 'matcherSuite', ['references'])).toEqual([]);
|
||||
expect(typeLinks('ext/ext.go', 'Param', ['references'])).toEqual([]);
|
||||
});
|
||||
|
||||
it('leaves a type a function declares to name matching, as before, beside such a dot import', () => {
|
||||
expect(links('ext/local.go', 'Attached')).toEqual([
|
||||
'ctx.QueryProgressReporters() -> ext/local.go::progressManager::QueryProgressReporters',
|
||||
]);
|
||||
});
|
||||
|
||||
it('never links a same-named type of a package the file does not import', () => {
|
||||
const decoys = cg!.getNodesInFile('aaa/aaa.go').filter((n) => n.kind !== 'file');
|
||||
expect(decoys.map((n) => n.qualifiedName).sort()).toEqual([
|
||||
'Controller',
|
||||
'Controller::AllConfigs',
|
||||
'Controller::UserConfigs',
|
||||
'Gomega',
|
||||
'Gomega::Expect',
|
||||
'Level',
|
||||
'Level::Label',
|
||||
'Manager',
|
||||
'Manager::Save',
|
||||
]);
|
||||
for (const decoy of decoys) {
|
||||
// A struct whose promoted methods satisfy an interface does implement
|
||||
// it, whichever package declares it: those edges are synthesized.
|
||||
expect(
|
||||
cg!.getIncomingEdges(decoy.id).filter((e) => e.kind !== 'contains' && e.provenance !== 'heuristic'),
|
||||
decoy.qualifiedName,
|
||||
).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isImportableKind,
|
||||
CPP_DEFINE_SIGNATURE,
|
||||
} from './types';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, isImportShadowedByJsBinding } from './name-matcher';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, isImportShadowedByJsBinding } from './name-matcher';
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
|
||||
@@ -3653,6 +3653,9 @@ export class ReferenceResolver {
|
||||
// imported unaliased as `go.etcd.io/etcd/client/v3`), a type found by its
|
||||
// bare name is some other package's namesake.
|
||||
if (ref.language === 'go' && isGoUnknownQualified(ref, this.context)) return null;
|
||||
// One embedded bare that only a package the file dot-imports declares is
|
||||
// that package's, not a namesake as near the file.
|
||||
if (ref.language === 'go') return goDotImportedSupertype(result, ref, this.context);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+108
-34
@@ -2538,9 +2538,11 @@ export function isGoUnknownQualified(ref: UnresolvedRef, context: ResolutionCont
|
||||
* 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
|
||||
* the one of that name in the reference's own package for a bare name (or in
|
||||
* a package its file dot-imports, goBareTypeDir), or in the imported project
|
||||
* package for `pkg.T`. Without one there, a method or function of the name is
|
||||
* nothing the reference means, nor is any type a bare name only a package
|
||||
* from outside the project the file dot-imports can give it. 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,
|
||||
@@ -2566,8 +2568,10 @@ export function goTypePositionTarget(result: ResolvedRef, ref: UnresolvedRef, co
|
||||
} 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 (bare) {
|
||||
pkgDir = goBareTypeDir(name, ref.filePath, context);
|
||||
if (pkgDir === null) return null;
|
||||
} 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) : [];
|
||||
@@ -19916,15 +19920,19 @@ function goImportPackageDir(qualifier: string, filePath: string, context: Resolu
|
||||
|
||||
/**
|
||||
* The package directory that declares a Go type spelled `raw` in `filePath`:
|
||||
* the file's own package for a bare name (`server`, `*Store`), the imported
|
||||
* package for a qualified one (`store.Manager`) when it is a project package.
|
||||
* the file's own package for a bare name (`server`, `*Store`), or a package
|
||||
* the file dot-imports that declares it (goBareTypeDir), the imported package
|
||||
* for a qualified one (`store.Manager`) when it is a project package.
|
||||
* Undefined when that can't be told, which leaves resolution as it was.
|
||||
*/
|
||||
function goDeclaredTypePackage(raw: string | undefined, filePath: string, context: ResolutionContext): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
const name = raw.replace(/[*&\s]/g, '');
|
||||
const dot = name.indexOf('.');
|
||||
if (dot < 0) return goPackageDir(filePath);
|
||||
if (dot < 0) {
|
||||
const bare = /^[A-Za-z_]\w*/.exec(name)?.[0];
|
||||
return bare ? goBareTypeDir(bare, filePath, context) ?? undefined : goPackageDir(filePath);
|
||||
}
|
||||
return goImportPackageDir(name.slice(0, dot), filePath, context) ?? undefined;
|
||||
}
|
||||
|
||||
@@ -20021,7 +20029,7 @@ function goAliasTarget(typeNode: Node, context: ResolutionContext): { name: stri
|
||||
const m = head && /^\s*(?:\*\s*)?([A-Za-z_]\w*)(?:\s*\.\s*([A-Za-z_]\w*))?\s*(?:\[.*\])?\s*;?\s*$/.exec(decl.slice(head[0].length));
|
||||
let target: { name: string; pkgDir: string } | null | undefined;
|
||||
if (m) {
|
||||
const pkgDir = m[2] ? goImportPackageDir(m[1]!, typeNode.filePath, context) : goPackageDir(typeNode.filePath);
|
||||
const pkgDir = m[2] ? goImportPackageDir(m[1]!, typeNode.filePath, context) : goBareTypeDir(m[1]!, typeNode.filePath, context);
|
||||
const name = m[2] ?? m[1]!;
|
||||
target = pkgDir == null || (!m[2] && GO_BUILTIN_FIELD_TYPES.has(name)) ? null : { name, pkgDir };
|
||||
} else {
|
||||
@@ -20069,7 +20077,7 @@ function goEmbeddedTypes(typeNode: Node, context: ResolutionContext): Array<{ na
|
||||
for (const member of body.split(/[\n;]/)) {
|
||||
const m = /^\s*\*?\s*([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?\s*(?:\[[^\]]*\])?\s*(?:`[^`]*`|"[^"]*")?\s*$/.exec(member);
|
||||
if (!m) continue;
|
||||
const pkgDir = m[2] ? goImportPackageDir(m[1]!, typeNode.filePath, context) : goPackageDir(typeNode.filePath);
|
||||
const pkgDir = m[2] ? goImportPackageDir(m[1]!, typeNode.filePath, context) : goBareTypeDir(m[1]!, typeNode.filePath, context);
|
||||
if (pkgDir == null) continue;
|
||||
embedded.push({ name: m[2] ?? m[1]!, pkgDir });
|
||||
}
|
||||
@@ -20125,11 +20133,12 @@ export function matchGoAssertedCall(ref: UnresolvedRef, context: ResolutionConte
|
||||
* declares it, found where Go finds it: in the file's own package or a
|
||||
* package it dot-imports for a bare name, in the imported package for a
|
||||
* qualified one. Null for a type found in none — one from outside the
|
||||
* project (`http.Flusher`), a predeclared one (`error`) or an alias of such a
|
||||
* type: none has a method the project declares. Undefined for one it can't
|
||||
* tell: a type literal (`interface{ Flush() }`), a name no package of the
|
||||
* project declares (a type declared inside a function, a type parameter), a
|
||||
* type the imported project package doesn't hold.
|
||||
* project (`http.Flusher`, or a bare name only a package from outside it
|
||||
* that the file dot-imports can give it), a predeclared one (`error`) or an
|
||||
* alias of such a type: none has a method the project declares. Undefined
|
||||
* for one it can't tell: a type literal (`interface{ Flush() }`), a name no
|
||||
* package of the project declares (a type declared inside a function, a type
|
||||
* parameter), a type the imported project package doesn't hold.
|
||||
*/
|
||||
function goWrittenType(written: string, ref: UnresolvedRef, context: ResolutionContext): GoProjectType | null | undefined {
|
||||
// `KVServer`, `*pipe`, `storage.Store`, `*List[int]`: anything else is a type literal.
|
||||
@@ -20143,8 +20152,9 @@ function goWrittenType(written: string, ref: UnresolvedRef, context: ResolutionC
|
||||
if (!dir) return null;
|
||||
if (!declares(dir)) return undefined;
|
||||
} else {
|
||||
dir = [goPackageDir(ref.filePath), ...goDotImportDirs(ref.filePath, context)].find(declares);
|
||||
if (!dir) return GO_BUILTIN_FIELD_TYPES.has(name!) ? null : undefined;
|
||||
dir = goBareTypeDir(name!, ref.filePath, context);
|
||||
if (dir === null) return null;
|
||||
if (!declares(dir)) return GO_BUILTIN_FIELD_TYPES.has(name!) ? null : undefined;
|
||||
}
|
||||
// An alias of a type from outside the project (`type Ctx = context.Context`)
|
||||
// has that type's methods, none the project declares.
|
||||
@@ -20338,7 +20348,8 @@ function matchGoPackageValueCall(receiver: string, method: string, ref: Unresolv
|
||||
const dot = named.indexOf('.');
|
||||
const name = named.slice(dot + 1);
|
||||
if (dot < 0 && GO_BUILTIN_FIELD_TYPES.has(name)) return null;
|
||||
const pkgDir = dot < 0 ? goPackageDir(type.file) : goImportPackageDir(named.slice(0, dot), type.file, context);
|
||||
const pkgDir = dot < 0 ? goBareTypeDir(name, type.file, context) : goImportPackageDir(named.slice(0, dot), type.file, context);
|
||||
if (pkgDir === null && dot < 0) return null;
|
||||
if (!pkgDir || goPackageTypes(name, pkgDir, context).length === 0) return undefined;
|
||||
// A project type whose methods aren't found is left to the strategies
|
||||
// below: kubernetes' `var UnstructuredJSONScheme runtime.Codec =
|
||||
@@ -20402,7 +20413,7 @@ function goValueType(value: string, filePath: string, context: ResolutionContext
|
||||
const written = qualifier !== undefined ? `${qualifier}.${name}` : name!;
|
||||
if (open === '{') return GO_TYPE_KEYWORDS.has(name!) ? null : { written, file: filePath };
|
||||
// A call is a conversion when it names a type of its package.
|
||||
const dir = qualifier !== undefined ? goImportPackageDir(qualifier, filePath, context) : goPackageDir(filePath);
|
||||
const dir = qualifier !== undefined ? goImportPackageDir(qualifier, filePath, context) : goBareTypeDir(name!, filePath, context);
|
||||
return dir && goPackageTypes(name!, dir, context).length > 0 ? { written, file: filePath } : undefined;
|
||||
}
|
||||
|
||||
@@ -20564,24 +20575,83 @@ function goClosingBracket(text: string, open: number): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const GO_DOT_IMPORTS = new WeakMap<ResolutionContext, Map<string, string[]>>();
|
||||
/** The packages a Go file dot-imports. */
|
||||
interface GoDotImports {
|
||||
/** The project's, by directory: their exported names the file reads bare. */
|
||||
dirs: string[];
|
||||
/** Whether one is a package from outside the project's modules (`. "github.com/onsi/gomega"`). */
|
||||
outside: boolean;
|
||||
/** The type names the file's text declares, inside its functions too: read when first needed. */
|
||||
types?: Set<string>;
|
||||
}
|
||||
|
||||
/** The project packages a Go file dot-imports (`import . "example.com/app/storage"`), by directory: their names it reads bare. */
|
||||
function goDotImportDirs(filePath: string, context: ResolutionContext): string[] {
|
||||
const GO_DOT_IMPORTS = new WeakMap<ResolutionContext, Map<string, GoDotImports>>();
|
||||
|
||||
/** The packages a Go file dot-imports (`import . "example.com/app/storage"`). */
|
||||
function goDotImports(filePath: string, context: ResolutionContext): GoDotImports {
|
||||
let memo = GO_DOT_IMPORTS.get(context);
|
||||
if (!memo) GO_DOT_IMPORTS.set(context, (memo = new Map()));
|
||||
let dirs = memo.get(filePath);
|
||||
if (dirs) return dirs;
|
||||
dirs = [];
|
||||
let found = memo.get(filePath);
|
||||
if (found) return found;
|
||||
found = { dirs: [], outside: false };
|
||||
for (const line of context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? []) {
|
||||
// The imports come before any other declaration.
|
||||
if (/^(?:func|type|var|const)\b/.test(line)) break;
|
||||
const dot = /^\s*(?:import\s+)?\.\s+"([^"]+)"/.exec(line);
|
||||
const dir = dot ? context.getGoPackageDir?.(dot[1]!, filePath) : null;
|
||||
if (dir) dirs.push(dir);
|
||||
if (!dot || !context.getGoPackageDir) continue;
|
||||
const dir = context.getGoPackageDir(dot[1]!, filePath);
|
||||
if (dir) found.dirs.push(dir);
|
||||
// A relative or `/internal/` path is the project's own, mapped or not.
|
||||
else if (!dot[1]!.startsWith('.') && !dot[1]!.includes('/internal/')) found.outside = true;
|
||||
}
|
||||
memo.set(filePath, dirs);
|
||||
return dirs;
|
||||
memo.set(filePath, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory of the package that declares a Go type written bare in
|
||||
* `filePath` (`Controller`, `*Manager`), as Go reads the name: the file's own
|
||||
* package, else a package of the project the file dot-imports that declares
|
||||
* it (harbor's config tests read `Controller` from `. ".../controller/config"`).
|
||||
* Null for an exported name that only a package from outside the project the
|
||||
* file dot-imports can give it (`. "github.com/onsi/gomega"`): no type of the
|
||||
* project's. The file's own package when no package declares the name, as
|
||||
* before: a type the index doesn't hold there, like one a function declares
|
||||
* (ginkgo's `type CompleteSpecContext interface {…}` inside a test), which
|
||||
* hides a dot import's.
|
||||
*/
|
||||
function goBareTypeDir(name: string, filePath: string, context: ResolutionContext): string | null {
|
||||
const own = goPackageDir(filePath);
|
||||
const dots = goDotImports(filePath, context);
|
||||
if ((dots.dirs.length === 0 && !dots.outside) || goPackageTypes(name, own, context).length > 0) return own;
|
||||
const dir = dots.dirs.find((d) => goPackageTypes(name, d, context).length > 0);
|
||||
if (dir !== undefined) return dir;
|
||||
if (!dots.outside || !/^[A-Z]\w*$/.test(name)) return own;
|
||||
if (!dots.types) {
|
||||
dots.types = new Set();
|
||||
for (const line of context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? []) {
|
||||
for (const m of line.replace(/\/\/.*$/, '').matchAll(/(?:^|[^\w.])type\s+([A-Za-z_]\w*)/g)) dots.types.add(m[1]!);
|
||||
}
|
||||
}
|
||||
return dots.types.has(name) ? own : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Go embedding written bare (`*Manager`) names when the file's own
|
||||
* package doesn't declare the name: the type of the package the file
|
||||
* dot-imports that does (goBareTypeDir), not another package's namesake name
|
||||
* matching found as near the file, or nothing for a name only a package from
|
||||
* outside the project can give the file. Any other is left as it is.
|
||||
*/
|
||||
export function goDotImportedSupertype(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
const name = ref.referenceName;
|
||||
if (!/^[A-Za-z_]\w*$/.test(name)) return result;
|
||||
const dir = goBareTypeDir(name, ref.filePath, context);
|
||||
if (dir === goPackageDir(ref.filePath) || goRefQualification(ref, context).written !== undefined) return result;
|
||||
if (dir === null) return null;
|
||||
const target = context.getNodeById?.(result.targetNodeId);
|
||||
if (target && goPackageDir(target.filePath) === dir) return result;
|
||||
return { ...result, targetNodeId: preferCallSiteFile(goPackageTypes(name, dir, context), ref.filePath)[0]!.id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20814,8 +20884,8 @@ function goNamedTypeDecls(t: GoTypeText, context: ResolutionContext): { name: st
|
||||
if (second === undefined) {
|
||||
if (t.params?.has(name)) return undefined;
|
||||
if (GO_BUILTIN_FIELD_TYPES.has(name)) return null;
|
||||
dir = [goPackageDir(t.file), ...goDotImportDirs(t.file, context)].find((d) => goPackageTypes(name, d, context).length > 0);
|
||||
if (dir === undefined) return undefined;
|
||||
dir = goBareTypeDir(name, t.file, context);
|
||||
if (dir === null) return null;
|
||||
} else {
|
||||
dir = goImportPackageDir(first!, t.file, context);
|
||||
if (dir === undefined) return undefined;
|
||||
@@ -22481,9 +22551,13 @@ function matchGoFieldChainCall(
|
||||
const m = line.match(fieldTypeRe);
|
||||
if (!m || !m[1]) continue;
|
||||
const rawType = m[1];
|
||||
// The package that declares the field's type: the struct's own for a
|
||||
// bare type name, the imported one for a package-qualified name.
|
||||
let pkgDir = goPackageDir(s.filePath);
|
||||
// The package that declares the field's type: for a bare type name the
|
||||
// struct's own, or one its file dot-imports (harbor's config tests read
|
||||
// `controller Controller` from `. ".../controller/config"`) — none of
|
||||
// the project's for a name only a package from outside it can give the
|
||||
// file — the imported one for a package-qualified name.
|
||||
let pkgDir = rawType.includes('.') ? undefined : goBareTypeDir(rawType, s.filePath, context);
|
||||
if (pkgDir === null) continue;
|
||||
// A package-qualified field type (`http.Handler`, `sql.DB`) is only
|
||||
// followed when the package is one of the project's: stripping the
|
||||
// qualifier and matching the bare name would conflate a stdlib or
|
||||
|
||||
Reference in New Issue
Block a user