diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f93154ed..92bbf5895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.value(null)` or Riverpod's `final reportProvider = Family()`, 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;`, 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. diff --git a/__tests__/go-dot-import-field-type.test.ts b/__tests__/go-dot-import-field-type.test.ts new file mode 100644 index 000000000..69e062646 --- /dev/null +++ b/__tests__/go-dot-import-field-type.test.ts @@ -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 = { + '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!; + } + + /** + * ` -> ::` 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(); + } + + /** ` ::` 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([]); + } + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 24872d9e7..8c5a5899b 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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; } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 215a06c2a..ee8e44eee 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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>(); +/** 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; +} -/** 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>(); + +/** 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