From 6b967c83ea6b88b256c343c9b3372dbad25f5179 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 4eb9e73572..3ce251f6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In React with TypeScript, a component that renders a generic component and passes it type arguments, like outline's ` items={…} />` or excalidraw's ` …>`, now links to the component it renders. Before, a tag followed by type arguments was skipped, so the generic component looked unused by the screens that render it, and `codegraph_explore`'s call paths, callers and impact stopped short of it. Re-index React projects after upgrading. - In React, a component that a file declares itself as a value is now what that file's JSX renders, whatever else in the repository shares its name. A lazily loaded page, like `const RegisterPage = Loadable(lazy(() => import('./pages/Register')))`, a `dynamic(() => import(…))` component or one loaded only in development (`IS_DEV ? lazy(…) : null`), links to the component its module exports, including the one its loader picks and one an `export default observer(Login)` wraps; a wrapped component like `const Settings = observer(function Settings() {…})`, `React.memo(Rows, areEqual)` or `forwardRef(ComboboxInner) as {…}` links to the function it wraps; and an alias like `const DialogPortal = DialogPrimitive.Portal` links to its own declaration. Before, `` was linked to any lone component of that name elsewhere, often in another app of the same repository, so `codegraph_explore`, callers and impact followed it into the wrong app while the page it really renders had no caller. React Router layouts declared this way follow the same rule, and a TSX type named like a value the file declares now stays in that file. Re-index React projects after upgrading. - In React, a JSX tag imported from one of your own modules now links to the component that module exports under that name, read the way a lazily loaded page's module already is. That includes going through an `index` file or barrel that passes the component on, like outline's `Sidebar/index.ts` with `import Sidebar from './App'; export default Sidebar` or its `Avatar/index.ts` with `const Avatar = AvatarWithHoverCard`, and past a wrapper such as `export default observer(AppSidebar)`, `React.memo(StaticCanvas, areEqual)` or mastodon's `const MemoizedBlurhash = memo(Blurhash)`. Before, a tag was matched by its name and the import only broke ties, so outline's `` linked to an unrelated styled `Sidebar` in a sibling file, a default import under another name linked to whatever other component its module exported, and an import through a barrel could land on another app's component of the same name. A tag whose module exports an unnamed component, like `export default () => …`, now links to nothing instead of to a namesake elsewhere. A lazily loaded React Router page exported as such a value or wrapper links to the component it holds too, the one with the markup. `codegraph sync` and the file watcher redraw these links when only the file that passes the component on changes. Re-index React 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 0000000000..ec2f7ea636 --- /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 cac05d6ce9..2f1e008669 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -2767,6 +2767,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 */ @@ -2861,7 +2871,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); // Not a local bound from a type assertion that is named like one: // grpc-go's `parser.ParseConfig(…)` after `parser, ok :=