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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<PaginatedList<Document> items={…} />` or excalidraw's `<DropdownMenuItemContentRadio<"contain" | "overlap"> …>`, 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, `<RegisterPage />` 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 `<Sidebar>` 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

Expand Down
172 changes: 172 additions & 0 deletions __tests__/go-stdlib-named-package.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
'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');
});
});
14 changes: 13 additions & 1 deletion src/resolution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 :=
Expand Down