mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:43:17 +08:00
A Go reference written through a package qualifier was dropped as a standard-library reference whenever the qualifier matched a name in the built-in list (errors, log, types, parser, encoding, template, ...). The check looked at the name alone, never at what the file imports under that name, so a project with its own package of such a name lost nearly every call into it: harbor's errors.New(...) through its lib/errors, prometheus's parser.Inspect(...) through promql/parser. What the file imports now decides before the name does. A qualifier the file imports, by its last path segment, the name goimports assumes for an unaliased import, or an alias, from a package of one of the project's own modules is a project package, and the reference goes on to normal resolution. A file that imports the standard library's package of that name, and a qualifier the file doesn't import at all, are skipped as before. Validation: the new test file has 7 cases, 4 of which fail on main; all pass here, with the native kernel and with CODEGRAPH_KERNEL=0. A/B against main: harbor 296f304 (its Go tree, 1,611 Go files) +2,889 edges, prometheus a2fda0e +454, 0 removed and 0 retargeted on both, nodes unchanged. Every added edge was checked against its source: the line spells qualifier.Name and the file imports the target's package directory under that qualifier from inside the module (3,343 of 3,343), and a sample of 10 was read by eye. gin and cobra, and the controls express and flask, are byte-identical. Index CPU on harbor, median of 3 interleaved: 40.0 s on main, 39.9 s here. Original PR #2368 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
4.4 KiB
TypeScript
173 lines
4.4 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|