mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
53fe53deb5
A Go method is only reachable through a value or a method expression, so a
bare call such as `relogin(ctx)` on a func parameter was being bound to a
same-named method in its own package, or (when capitalised) to an exported
method of a package the file does not import. Extend the #1714 receiver-less
check to Go, reading the call site back from source so the
`pkg.Factory().Method()` chain that reaches the resolver as a bare `Method`
ref keeps its edge.
Claude-Session: https://claude.ai/code/session_01MuSia5rNjiWCqzqW9iTJGD
(cherry picked from commit 812cf0d45f)
Co-authored-by: Max Hsu <maxmilian@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
/**
|
|
* In Go a receiver-less call — `relogin(ctx)` — can never reach a method:
|
|
* a method is only callable through a value (`l.relogin(ctx)`) or a method
|
|
* expression (`(*Loop).relogin`), never by its bare name. So a bare call to a
|
|
* function parameter or a local func value must not be bound to a same-named
|
|
* method, whether it sits in the same package or — capitalised — in a package
|
|
* the file does not import (#1857). Real method calls, including the
|
|
* `pkg.Factory().Method()` chain the extractor emits under the bare method
|
|
* name, keep their edges.
|
|
*/
|
|
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import CodeGraph from '../src/index';
|
|
|
|
let tempDir: string;
|
|
let cg: CodeGraph | null = null;
|
|
|
|
async function callees(files: Record<string, string>, fromName: string): Promise<string[]> {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1857-'));
|
|
for (const [rel, source] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(tempDir, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(tempDir, rel), source);
|
|
}
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
cg.resolveReferences();
|
|
const from = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.name === fromName)!;
|
|
expect(from).toBeDefined();
|
|
return cg
|
|
.getOutgoingEdges(from.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => cg!.getNode(e.target))
|
|
.filter((n): n is NonNullable<typeof n> => !!n)
|
|
.map((n) => `${n.kind}:${n.name}@${n.filePath}`);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = null;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const GO_MOD = 'module example.com/app\n\ngo 1.22\n';
|
|
|
|
describe('a receiver-less Go call never binds to a method (#1857)', () => {
|
|
it('does not bind a call to a func parameter onto a same-package method', async () => {
|
|
const out = await callees(
|
|
{
|
|
'go.mod': GO_MOD,
|
|
'loop/loop.go': [
|
|
'package loop',
|
|
'',
|
|
'type Loop struct{}',
|
|
'',
|
|
'func (l *Loop) relogin() error { return nil }',
|
|
'',
|
|
].join('\n'),
|
|
'loop/sched.go': [
|
|
'package loop',
|
|
'',
|
|
'func runScheduler(relogin func() error) error {',
|
|
'\tdefer relogin()',
|
|
'\tgo relogin()',
|
|
'\treturn relogin()',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
},
|
|
'runScheduler'
|
|
);
|
|
expect(out.filter((c) => c.startsWith('method:'))).toEqual([]);
|
|
});
|
|
|
|
it('does not bind a capitalised bare call onto an exported method of an unimported package', async () => {
|
|
const out = await callees(
|
|
{
|
|
'go.mod': GO_MOD,
|
|
'impulse/loop.go': [
|
|
'package impulse',
|
|
'',
|
|
'type Loop struct{}',
|
|
'',
|
|
'func (l *Loop) Relogin() error { return nil }',
|
|
'',
|
|
].join('\n'),
|
|
'synapse/sched.go': [
|
|
'package synapse',
|
|
'',
|
|
'func RunScheduler(Relogin func() error) error {',
|
|
'\tif err := Relogin(); err != nil {',
|
|
'\t\treturn err',
|
|
'\t}',
|
|
'\treturn nil',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
},
|
|
'RunScheduler'
|
|
);
|
|
expect(out.filter((c) => c.startsWith('method:'))).toEqual([]);
|
|
});
|
|
|
|
it('keeps a bare call onto a same-package function', async () => {
|
|
const out = await callees(
|
|
{
|
|
'go.mod': GO_MOD,
|
|
'loop/loop.go': [
|
|
'package loop',
|
|
'',
|
|
'type Loop struct{}',
|
|
'',
|
|
'func (l *Loop) flush() {}',
|
|
'',
|
|
'func flush() {}',
|
|
'',
|
|
'func run() {',
|
|
'\tflush()',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
},
|
|
'run'
|
|
);
|
|
expect(out).toContain('function:flush@loop/loop.go');
|
|
expect(out.filter((c) => c.startsWith('method:'))).toEqual([]);
|
|
});
|
|
|
|
it('keeps a real method call through a receiver', async () => {
|
|
const out = await callees(
|
|
{
|
|
'go.mod': GO_MOD,
|
|
'loop/loop.go': [
|
|
'package loop',
|
|
'',
|
|
'type Loop struct{}',
|
|
'',
|
|
'func (l *Loop) relogin() error { return nil }',
|
|
'',
|
|
'func (l *Loop) run() error {',
|
|
'\treturn l.relogin()',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
},
|
|
'run'
|
|
);
|
|
expect(out).toContain('method:relogin@loop/loop.go');
|
|
});
|
|
|
|
it('keeps a package-qualified factory chain onto the method', async () => {
|
|
const out = await callees(
|
|
{
|
|
'go.mod': GO_MOD,
|
|
'service/table.go': [
|
|
'package service',
|
|
'',
|
|
'type Table struct{}',
|
|
'',
|
|
'func (t *Table) Reload() error { return nil }',
|
|
'',
|
|
'func SysTable() *Table { return &Table{} }',
|
|
'',
|
|
].join('\n'),
|
|
'controller/ctl.go': [
|
|
'package controller',
|
|
'',
|
|
'import "example.com/app/service"',
|
|
'',
|
|
'func Handle() error {',
|
|
'\treturn service.SysTable().Reload()',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
},
|
|
'Handle'
|
|
);
|
|
expect(out).toContain('method:Reload@service/table.go');
|
|
});
|
|
});
|