mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 06:22:38 +08:00
16bc05a208
Receiver inference read `v := T{}`, `var v T` and parameters, never a
local bound from a type assertion. So `flusher.Flush()` after
`flusher, ok := w.(http.Flusher)` went to whichever project method name
matching picked, and calls like etcd's `fi.Action()` after
`fi := s.kvindex.(*fakeIndex)` linked to nothing.
A Go local whose declaration in scope at the call binds the value of a
type assertion (`v := x.(T)`, `v, ok := x.(T)`, `var v, _ = x.(T)`) now
has the asserted type, found where Go finds it, as #2444 does for a call
made through the assertion itself (`x.(T).M()`). Calls, method values and
field chains through the local (`wr.job.Start()`) reach the type's own
method, its interface's, or one embedding promotes into it. The binding is
the one the scope reader from #2448 finds in scope at the call: an `if`
header's ends with its statement, and a nearer declaration in a block that
has ended does not hide it. A type switch's variable and a value that only
starts with an assertion (`x.(T).field`) resolve as before.
A type from outside the project, a predeclared type or an alias of an
outside type links nothing, as for the assertion itself. A type the index
can't tell is left to name matching, as before: a type literal
(`interface{ GetDeadline() time.Time }`) or a type declared inside a
function (grpc-go's `internalInfo` and `grpcstatus`). A local bound from an
assertion and named like a standard-library package
(`parser, ok := builder.(balancer.ConfigParser)`) is no longer dropped as
a call into that package.
On etcd, harbor, kratos and grpc-go, 358 call sites change. 216 gain a
link, 38 move to the asserted type's method and 89 keep their target, now
resolved through the asserted type. 15 lose a link that went through a
type from outside the project, or from a field read to a same-named
function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
/**
|
|
* A Go local bound from a type assertion has the asserted type:
|
|
*
|
|
* if f, ok := w.(http.Flusher); ok {
|
|
* f.Flush() // http.Flusher's, nothing in the project
|
|
* }
|
|
* wr := v.(*Wrapper)
|
|
* wr.Flush() // what *Wrapper has, promoted from *storage.Base
|
|
*
|
|
* Receiver inference read `v := T{}`, `var v T` and parameters, never
|
|
* `v := x.(T)` or `v, ok := x.(T)`, so such a call went to whichever project
|
|
* method name matching picked. The asserted type is now found where Go finds
|
|
* it, as for a call made through the assertion itself (`x.(T).M()`): in the
|
|
* file's own package or one it dot-imports for a bare name, in the imported
|
|
* project package for `pkg.T`, an alias being the type it names. A type from
|
|
* outside the project, a predeclared one or an alias of such a type links
|
|
* nothing. A type literal, or a type declared inside a function, says nothing
|
|
* the index can follow: the call goes to name matching, as before. The
|
|
* binding is the one in scope at the call: an assertion in an `if` header
|
|
* does not reach a later variable of that name, nor does a nearer
|
|
* declaration in a block that has ended hide it. A local named like a
|
|
* standard-library package (`parser`) is the local.
|
|
*/
|
|
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. Namesakes sit where name matching looked
|
|
* first: an `alpha` package that sorts ahead of `storage`, and a same-file
|
|
* `sink` with each method name.
|
|
*/
|
|
const FILES: Record<string, string> = {
|
|
'go.mod': 'module example.com/app\n\ngo 1.22\n',
|
|
'alpha/alpha.go': `package alpha
|
|
|
|
type Store interface {
|
|
Fetch(key string) string
|
|
}
|
|
|
|
type sink struct{}
|
|
|
|
func (s *sink) Fetch(key string) string { return "" }
|
|
|
|
func (s *sink) Flush() error { return nil }
|
|
|
|
func (s *sink) Read(p []byte) (int, error) { return 0, nil }
|
|
|
|
func (s *sink) Error() string { return "" }
|
|
|
|
func (s *sink) Len() int { return 0 }
|
|
|
|
func (s *sink) Start() {}
|
|
|
|
type Ctx struct{}
|
|
|
|
func (c *Ctx) Done() <-chan struct{} { return nil }
|
|
|
|
type BufFlusher struct{}
|
|
|
|
func (b *BufFlusher) Flush() error { return nil }
|
|
`,
|
|
'storage/storage.go': `package storage
|
|
|
|
type Store interface {
|
|
Fetch(key string) string
|
|
}
|
|
|
|
type Base struct{}
|
|
|
|
func (b *Base) Flush() error { return nil }
|
|
`,
|
|
'store/store.go': `package store
|
|
|
|
import (
|
|
"context"
|
|
|
|
"example.com/app/storage"
|
|
)
|
|
|
|
type Reader interface {
|
|
Read(p []byte) (int, error)
|
|
}
|
|
|
|
type ReadCloser interface {
|
|
Reader
|
|
Close() error
|
|
}
|
|
|
|
type Job struct{}
|
|
|
|
func (j *Job) Start() {}
|
|
|
|
type Wrapper struct {
|
|
*storage.Base
|
|
job *Job
|
|
}
|
|
|
|
type List[T any] struct{}
|
|
|
|
func (l *List[T]) Len() int { return 0 }
|
|
|
|
type Ctx = context.Context
|
|
|
|
type Keeper = storage.Store
|
|
`,
|
|
'store/use.go': `package store
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"example.com/app/storage"
|
|
)
|
|
|
|
type sink struct{}
|
|
|
|
func (s *sink) Flush() error { return nil }
|
|
|
|
func (s *sink) Fetch(key string) string { return "" }
|
|
|
|
func (s *sink) Read(p []byte) (int, error) { return 0, nil }
|
|
|
|
func (s *sink) Error() string { return "" }
|
|
|
|
func (s *sink) Drain() {}
|
|
|
|
func register(fn any) {}
|
|
|
|
func lookup(key string) any { return nil }
|
|
|
|
func Flusher(w http.ResponseWriter) {
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
func Promoted(v any) {
|
|
wr := v.(*Wrapper)
|
|
wr.Flush()
|
|
}
|
|
|
|
func Embedded(v any, p []byte) {
|
|
rc, ok := v.(ReadCloser)
|
|
if ok {
|
|
rc.Read(p)
|
|
}
|
|
}
|
|
|
|
func Qualified(v any) string {
|
|
var st, _ = v.(storage.Store)
|
|
return st.Fetch("k")
|
|
}
|
|
|
|
func Aliased(v any) string {
|
|
k := v.(Keeper)
|
|
return k.Fetch("k")
|
|
}
|
|
|
|
func Generic(v any) int {
|
|
l := v.(*List[int])
|
|
return l.Len()
|
|
}
|
|
|
|
func Quoted() string {
|
|
s, _ := lookup(")").(storage.Store)
|
|
return s.Fetch("k")
|
|
}
|
|
|
|
func Outside(v any) {
|
|
c := v.(Ctx)
|
|
c.Done()
|
|
if err, ok := v.(error); ok {
|
|
err.Error()
|
|
}
|
|
}
|
|
|
|
func Unseen(v any) {
|
|
d, _ := v.(interface{ Drain() })
|
|
d.Drain()
|
|
type drainer interface{ Drain() }
|
|
if dr, ok := v.(drainer); ok {
|
|
dr.Drain()
|
|
}
|
|
}
|
|
|
|
func Named(v any) string {
|
|
if parser, ok := v.(storage.Store); ok {
|
|
return parser.Fetch("k")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func Chain(v any) {
|
|
wr := v.(*Wrapper)
|
|
wr.job.Start()
|
|
}
|
|
|
|
func Values(v any, w http.ResponseWriter) {
|
|
st := v.(storage.Store)
|
|
register(st.Fetch)
|
|
f := w.(http.Flusher)
|
|
register(f.Flush)
|
|
}
|
|
|
|
func Assigned(v any) string {
|
|
var st storage.Store
|
|
st = v.(*sink)
|
|
return st.Fetch("k")
|
|
}
|
|
|
|
func Scoped(w http.ResponseWriter, sinks []*sink) {
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
for _, f := range sinks {
|
|
f.Drain()
|
|
}
|
|
}
|
|
|
|
func Shadowed(v any) string {
|
|
s, ok := v.(storage.Store)
|
|
if !ok {
|
|
s := &sink{}
|
|
return s.Fetch("fallback")
|
|
}
|
|
return s.Fetch("k")
|
|
}
|
|
|
|
func Commented(s *sink) {
|
|
// s, ok := v.(http.Flusher)
|
|
s.Drain()
|
|
}
|
|
|
|
func Switched(v any) {
|
|
switch x := v.(type) {
|
|
case *sink:
|
|
x.Drain()
|
|
}
|
|
}
|
|
`,
|
|
'dot/dot.go': `package dot
|
|
|
|
import . "example.com/app/storage"
|
|
|
|
func Get(v any) string {
|
|
s := v.(Store)
|
|
return s.Fetch("k")
|
|
}
|
|
`,
|
|
};
|
|
|
|
describe.each(['default', 'wasm'])('Go locals bound from a type assertion have the asserted type (%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-asserted-'));
|
|
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 === 'store/use.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;
|
|
});
|
|
|
|
/**
|
|
* `<source line> -> <target file>::<target qualified name>` for every edge
|
|
* of `kind` the function `name` in `file` has to a function or method.
|
|
*/
|
|
function links(file: string, name: string, kind: 'calls' | 'references' = 'calls'): string[] {
|
|
const fn = cg!.getNodesInFile(file).find((n) => n.qualifiedName === name && n.kind === 'function');
|
|
expect(fn, `${name} in ${file}`).toBeDefined();
|
|
const lines = FILES[file]!.split('\n');
|
|
return cg!
|
|
.getOutgoingEdges(fn!.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();
|
|
}
|
|
|
|
it('links nothing through an asserted type from outside the project', () => {
|
|
// Name matching took alpha's BufFlusher::Flush, whose owner shares the
|
|
// receiver's name.
|
|
expect(links('store/use.go', 'Flusher')).toEqual([]);
|
|
});
|
|
|
|
it("links the asserted project type's method: its own, an embedded type's, or one through a qualifier", () => {
|
|
// *Wrapper's Flush is promoted from *storage.Base; ReadCloser's Read from Reader.
|
|
expect(links('store/use.go', 'Promoted')).toEqual(['wr.Flush() -> storage/storage.go::Base::Flush']);
|
|
expect(links('store/use.go', 'Embedded')).toEqual(['rc.Read(p) -> store/store.go::Reader::Read']);
|
|
// `var v, _ = x.(T)` declares as `v, _ := x.(T)` does.
|
|
expect(links('store/use.go', 'Qualified')).toEqual(['return st.Fetch("k") -> storage/storage.go::Store::Fetch']);
|
|
expect(links('store/use.go', 'Generic')).toEqual(['return l.Len() -> store/store.go::List::Len']);
|
|
// A string argument holding a parenthesis is not the end of the call.
|
|
expect(links('store/use.go', 'Quoted')).toEqual([
|
|
'return s.Fetch("k") -> storage/storage.go::Store::Fetch',
|
|
's, _ := lookup(")").(storage.Store) -> store/use.go::lookup',
|
|
]);
|
|
});
|
|
|
|
it('follows an alias and a dot import to the type', () => {
|
|
expect(links('store/use.go', 'Aliased')).toEqual(['return k.Fetch("k") -> storage/storage.go::Store::Fetch']);
|
|
expect(links('dot/dot.go', 'Get')).toEqual(['return s.Fetch("k") -> storage/storage.go::Store::Fetch']);
|
|
});
|
|
|
|
it('links nothing through an alias of an outside type or a predeclared type', () => {
|
|
expect(links('store/use.go', 'Outside')).toEqual([]);
|
|
});
|
|
|
|
it('leaves a call through a type literal or a type declared in a function to name matching', () => {
|
|
// sink::Drain is the project's one Drain.
|
|
expect(links('store/use.go', 'Unseen')).toEqual([
|
|
'd.Drain() -> store/use.go::sink::Drain',
|
|
'dr.Drain() -> store/use.go::sink::Drain',
|
|
]);
|
|
});
|
|
|
|
it('takes a local named like a standard-library package for the local', () => {
|
|
expect(links('store/use.go', 'Named')).toEqual(['return parser.Fetch("k") -> storage/storage.go::Store::Fetch']);
|
|
});
|
|
|
|
it("reaches a field's method through an asserted local", () => {
|
|
expect(links('store/use.go', 'Chain')).toEqual(['wr.job.Start() -> store/store.go::Job::Start']);
|
|
});
|
|
|
|
it("links a method value taken from an asserted local as the call would", () => {
|
|
expect(links('store/use.go', 'Values', 'references')).toEqual([
|
|
'register(st.Fetch) -> storage/storage.go::Store::Fetch',
|
|
]);
|
|
});
|
|
|
|
it("leaves an assertion assigned to a declared variable that variable's type", () => {
|
|
// `st = v.(*sink)` declares nothing: st is still a storage.Store.
|
|
expect(links('store/use.go', 'Assigned')).toEqual(['return st.Fetch("k") -> storage/storage.go::Store::Fetch']);
|
|
});
|
|
|
|
it('reads the binding in scope at the call', () => {
|
|
// The `if` header's f ends with its block: the loop's f is a *sink.
|
|
expect(links('store/use.go', 'Scoped')).toEqual(['f.Drain() -> store/use.go::sink::Drain']);
|
|
// The block's s := &sink{} ends with the block; the asserted s is back.
|
|
expect(links('store/use.go', 'Shadowed')).toEqual([
|
|
'return s.Fetch("fallback") -> store/use.go::sink::Fetch',
|
|
'return s.Fetch("k") -> storage/storage.go::Store::Fetch',
|
|
]);
|
|
// A comment declares nothing.
|
|
expect(links('store/use.go', 'Commented')).toEqual(['s.Drain() -> store/use.go::sink::Drain']);
|
|
// A type switch's variable has its clause's type, which is not read: the
|
|
// call resolves as it did before.
|
|
expect(links('store/use.go', 'Switched')).toEqual(['x.Drain() -> store/use.go::sink::Drain']);
|
|
});
|
|
|
|
it('never links a namesake in another package or a same-file type', () => {
|
|
const decoys = [
|
|
...cg!.getNodesInFile('alpha/alpha.go'),
|
|
...cg!.getNodesInFile('store/use.go').filter((n) => ['sink::Flush', 'sink::Read', 'sink::Error'].includes(n.qualifiedName)),
|
|
].filter((n) => n.kind === 'method');
|
|
expect(decoys.length).toBe(12);
|
|
for (const decoy of decoys) {
|
|
expect(
|
|
cg!.getIncomingEdges(decoy.id).filter((e) => (e.kind === 'calls' || e.kind === 'references') && e.provenance !== 'heuristic'),
|
|
decoy.qualifiedName,
|
|
).toEqual([]);
|
|
}
|
|
});
|
|
});
|