From 07b426ce50dceea8300ba33dc32d17978b30f73c Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Tue, 6 Oct 2026 12:17:25 +0300 Subject: [PATCH] fix(go): a project package named like a standard-library package is not the standard library A call written through a package qualifier that matches a standard-library package name (`errors.New`, `log.Infof`, `types.Policy{}`) was dropped as a standard-library reference on the name alone, without looking at what the file imports under that name. A project that has its own `lib/errors` or `lib/log` lost nearly every call into them: on goharbor/harbor, 0 of 1,402 `errors.*` call sites and 1 of 1,325 `log.*` call sites written through the project's packages had an edge. The file's import now decides first: a qualifier imported, by its last path segment or an alias, from a package of one of the project's own modules is a project package. A file importing the real standard-library package, and a name the file does not import at all, are handled as before. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/go-stdlib-named-package.test.ts | 172 ++++++++++++++++++++++ src/resolution/index.ts | 14 +- 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 __tests__/go-stdlib-named-package.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d37c030..7d96dd4f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In JavaScript and TypeScript, an import written `import { default as AppRoot } from './routes/app/root'`, as in bulletproof-react's router, now counts as the module's default import, the same as `import AppRoot from './routes/app/root'`. Before, it was read as an import of an export named `default`, which no module has, so a call, a route or a JSX attribute that used `AppRoot` was matched by its name alone: it could link to another file's `AppRoot`, or to nothing when the default export has a name of its own. Svelte, Vue and Astro script blocks are read the same way. - In React, a component's JSX no longer links it to a class or component that only shares a name with one of its type arguments or with a variable of its own. A type in angle brackets, like `Document` in ` items={…} />`, `User` in `useState()` or `Entry` in a generic `(…) =>` component, is no longer read as a tag. A tag naming a variable or parameter the component sets itself, like `` after `const Content = isDropdown ? DropdownMenu.SubContent : ContextMenu.SubContent` or `` in `widgets.map((Widget) => )`, now links to nothing, unless the component declares a component of that name inside itself. Before, these linked to an unrelated class or component elsewhere in the repository: outline's document lists showed up among the callers of its `Document` model class, and its menus among the callers of the command bar's `Content`, so `codegraph_explore`, callers and impact followed renders that never happen. Re-index React projects after upgrading. - In JavaScript and TypeScript, a name imported from a package your `package.json` lists no longer links to a project symbol that only shares its name when `tsconfig.json` or `jsconfig.json` has a catch-all path alias, like `"*": ["./typings/*"]` or `"*": ["src/*", "node_modules/*"]`, or an alias that points the package at a file in `node_modules`, like `"lit/decorators": ["./node_modules/lit/decorators.js"]`. Such an alias made every package look like part of the project, so `import { Typography } from '@mui/material'` was linked to the project's own `Typography` and every lit `@property()` decorator to an unrelated class's `property` field, and `codegraph callers`, impact and `codegraph affected` listed code that never used them. An import the alias does map to a file of your project, like a `.d.ts` you keep for an untyped package or `components/Button` through `"*": ["src/*"]`, links as before. Re-index affected projects after upgrading. +- In Go, a call into one of your own packages that shares its name with a standard-library package, like a project's `lib/errors`, `pkg/log` or `model/user`, now links: `errors.New(…)` or `log.Infof(…)` in a file that imports your package reaches your function, so its callers and impact are no longer nearly empty. The same goes for a struct built from such a package, like `types.Policy{…}`. A file that imports the standard library's package of that name is unaffected. Re-index Go projects after upgrading. (#2368) ## [1.6.2] - 2026-10-03 diff --git a/__tests__/go-stdlib-named-package.test.ts b/__tests__/go-stdlib-named-package.test.ts new file mode 100644 index 000000000..ec2f7ea63 --- /dev/null +++ b/__tests__/go-stdlib-named-package.test.ts @@ -0,0 +1,172 @@ +/** + * A project package may carry a standard-library name — harbor's + * `src/lib/errors` and `src/lib/log`, a `pkg/types`, a `model/user`. What a + * file imports under that name decides whose `errors.New(…)` it writes: the + * project package's when the import path is inside one of the project's + * modules, the standard library's otherwise. + */ +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-stdname-')); + const files: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'lib/errors/errors.go': `package errors + +type Error struct { + Code string +} + +func New(msg string) *Error { + return &Error{Code: msg} +} +`, + 'lib/log/log.go': `package log + +func Printf(format string, args ...any) {} +`, + 'svc/project.go': `package svc + +import ( + "example.com/app/lib/errors" + "example.com/app/lib/log" +) + +func Create(name string) *errors.Error { + log.Printf("create %s", name) + return errors.New(name) +} + +func Literal() errors.Error { + return errors.Error{Code: "x"} +} +`, + 'svc/renamed.go': `package svc + +import ( + liberrors "example.com/app/lib/errors" +) + +func Renamed() *liberrors.Error { + return liberrors.New("x") +} +`, + 'svc/both.go': `package svc + +import ( + stderrors "errors" + + errors "example.com/app/lib/errors" +) + +func Ours() *errors.Error { + return errors.New("x") +} + +func Theirs() error { + return stderrors.New("y") +} +`, + 'svc/std.go': `package svc + +import ( + "errors" + "log" +) + +func Std() error { + log.Printf("std") + return errors.New("std") +} +`, + 'lib/log/v2/log.go': `package log + +func Infof(format string, args ...any) {} +`, + // Unaliased imports whose paths end in a major version are known as `log` + // and `errors`; only the first is a package of this module. + 'svc/versioned.go': `package svc + +import ( + "example.com/app/lib/log/v2" + "github.com/acme/errors/v2" +) + +func Versioned() error { + log.Infof("versioned") + return errors.New("outside") +} +`, + 'svc/variable.go': `package svc + +type bag struct{} + +func (b *bag) Len() int { return 0 } + +func Variable(errors *bag) int { + errors.New("x") + return errors.Len() +} +`, + }; + 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 }); +}); + +/** `kind file:name` of every edge leaving the function `fn` of `file`. */ +const edgesFrom = (file: string, fn: string) => { + const ids = cg.getNodesInFile(file).filter((n) => n.name === fn).map((n) => n.id); + return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind !== 'contains').map((e) => { + const target = cg.getNode(e.target)!; + return `${e.kind} ${target.filePath}:${target.qualifiedName}`; + }); +}; + +describe('Go project package named like a standard-library package', () => { + it('resolves a call written through its import', () => { + const edges = edgesFrom('svc/project.go', 'Create'); + expect(edges).toContain('calls lib/errors/errors.go:New'); + expect(edges).toContain('calls lib/log/log.go:Printf'); + }); + + it('resolves a composite literal of its type', () => { + expect(edgesFrom('svc/project.go', 'Literal')).toContain('instantiates lib/errors/errors.go:Error'); + }); + + it('resolves through an import alias', () => { + expect(edgesFrom('svc/renamed.go', 'Renamed')).toContain('calls lib/errors/errors.go:New'); + }); + + it('resolves when the alias is the standard-library name itself', () => { + expect(edgesFrom('svc/both.go', 'Ours')).toContain('calls lib/errors/errors.go:New'); + expect(edgesFrom('svc/both.go', 'Theirs')).toEqual([]); + }); + + it('resolves through an unaliased import whose path ends in a major version', () => { + expect(edgesFrom('svc/versioned.go', 'Versioned')).toEqual(['calls lib/log/v2/log.go:Infof']); + }); + + it('leaves a file importing the standard library package alone', () => { + expect(edgesFrom('svc/std.go', 'Std')).toEqual([]); + }); + + it('leaves a variable of that name alone when the file imports no such package', () => { + const edges = edgesFrom('svc/variable.go', 'Variable'); + expect(edges).not.toContain('calls lib/errors/errors.go:New'); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 20b42145b..4c5cd80b3 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -2709,6 +2709,16 @@ export class ReferenceResolver { return false; } + /** + * True when the Go file imports `qualifier` — by its last path segment or + * an alias — from a package of one of the project's own modules. + */ + private isGoProjectPackage(ref: UnresolvedRef, qualifier: string): boolean { + return this.context + .getImportMappings(ref.filePath, ref.language) + .some((imp) => imp.localName === qualifier && this.context.getGoPackageDir?.(imp.source, ref.filePath) != null); + } + /** * Check if reference is to a built-in or external symbol */ @@ -2803,7 +2813,9 @@ export class ReferenceResolver { // Go standard library packages — refs like "fmt.Println", "http.ListenAndServe", etc. if (ref.language === 'go') { const dotIdx = name.indexOf('.'); - if (dotIdx > 0) { + // What the file imports decides before the name does: a project package + // may be called `errors`, `log` or `types` too. + if (dotIdx > 0 && !this.isGoProjectPackage(ref, name.substring(0, dotIdx))) { const pkg = name.substring(0, dotIdx); if (GO_STDLIB_PACKAGES.has(pkg)) { return true;