Files
codegraph/__tests__/go-local-shadows-function.test.ts
T
Eric MinishandClaude Opus 5.5 e3f23cc4bc fix(go): a local's scope ends at its case clause, and a func-typed result's parameters are not locals (#2597)
Main's gate for a Go call to a local func value or parameter that
shadows a package function (PR 2658) reads scopes through the Go scope
reader, and two of its readings were too wide. A name declared in one
case of a switch or select stayed in scope in the clauses after it, and
the parameter names inside a func-typed result, like
func() func(run string), *func(...) or chan<- func(...), were read as
the function's own. Either made a later call to the package function
look shadowed and drop its link.

The PR's own earlier shadowing check is left out: the gate already
covers every case its tests pin. Go A/B on gin, cobra, etcd, prometheus
and hugo is byte-identical.

Fixes issue 2580.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:06:31 +00:00

100 lines
5.1 KiB
TypeScript

/**
* In Go a parameter or local declared in a function shadows a package-level
* function of the same name for the rest of its block, so a bare call to it
* — `run("x")` after `run := func…` or with a `run func(…)` parameter — calls
* the local func value, never the package-level `run` (#2580). The graph has
* no node for the local, so the call gets no edge. Outside the local's scope,
* the package-level function keeps its callers.
*/
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';
const GO_MOD = 'module example.com/app\n\ngo 1.22\n';
const RUN = 'func run(a, b int) int { return a + b }';
let tempDir: string;
let cg: CodeGraph | null = null;
async function edges(files: Record<string, string>, fromName: string): Promise<string[]> {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-2580-'));
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').find((n) => n.name === fromName)!;
expect(from).toBeDefined();
return cg
.getOutgoingEdges(from.id)
.filter((e) => e.kind === 'calls')
.map((e) => [e, cg!.getNode(e.target)] as const)
.filter(([, n]) => n?.kind === 'function')
.map(([e, n]) => `${e.kind} ${n!.name} L${e.line}`);
}
/** `package main` with the package-level `run`, `body`, and a `main` that calls `call`. */
function mainGo(body: string, call: string): Record<string, string> {
return { 'go.mod': GO_MOD, 'main.go': `package main\n\n${RUN}\n\n${body}\n\nfunc main() { ${call} }\n` };
}
afterEach(() => {
cg?.close();
cg = null;
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('a Go local or parameter shadows a same-named package function (#2580)', () => {
it.each([
['a := closure', 'func use() string {\n\trun := func(s string) string { return s }\n\treturn run("x")\n}', 'use()'],
['a func parameter', 'func use(run func(string) string) string {\n\treturn run("x")\n}', 'use(nil)'],
['a var closure', 'func use() string {\n\tvar run = func(s string) string { return s }\n\treturn run("x")\n}', 'use()'],
['an if-header local', 'func use() string {\n\tif run := func(s string) string { return s }; true {\n\t\treturn run("x")\n\t}\n\treturn ""\n}', 'use()'],
['a named result', 'func use() (run func(string) string) {\n\trun = func(s string) string { return s }\n\trun("x")\n\treturn\n}', 'use()'],
['a range value', 'func use(fns []func()) {\n\tfor _, run := range fns {\n\t\trun()\n\t}\n}', 'use(nil)'],
])('%s, in the same file', async (_label, body, call) => {
expect((await edges(mainGo(body, call), 'use')).filter((e) => / run /.test(e))).toEqual([]);
});
it('shadows a package function declared in another file', async () => {
const out = await edges(
{
'go.mod': GO_MOD,
'run.go': `package main\n\n${RUN}\n`,
'use.go': 'package main\n\nfunc use(run func(string) string) string {\n\treturn run("x")\n}\n\nfunc main() { use(nil) }\n',
},
'use'
);
expect(out).toEqual([]);
});
it('shadows a name a framework pattern would resolve', async () => {
const out = await edges(
{
'go.mod': GO_MOD,
'main.go': 'package main\n\nfunc errHandler(e error) {}\n\nfunc use() {\n\terrHandler := func(e error) {}\n\terrHandler(nil)\n}\n\nfunc main() { use() }\n',
},
'use'
);
expect(out).toEqual([]);
});
it.each([
['a call before the local is declared', 'func use() {\n\trun(1, 2)\n\trun := func() {}\n\trun()\n}', ['calls run L6']],
['a local initialised from the package function', 'func use() {\n\trun := run(1, 2)\n\t_ = run\n}', ['calls run L6']],
['a call after the block declaring the local', 'func use() {\n\tif true {\n\t\trun := func() {}\n\t\trun()\n\t}\n\trun(1, 2)\n}', ['calls run L10']],
['a sibling case clause', 'func use(x int) int {\n\tswitch x {\n\tcase 1:\n\t\trun := func(a, b int) int { return a }\n\t\treturn run(1, 2)\n\tcase 2:\n\t\treturn run(1, 2)\n\t}\n\treturn 0\n}', ['calls run L11']],
['a select default after a case declaring the local', 'func use(ch chan int) int {\n\tselect {\n\tcase <-ch:\n\t\trun := func(a, b int) int { return a }\n\t\treturn run(1, 2)\n\tdefault:\n\t\treturn run(1, 2)\n\t}\n}', ['calls run L11']],
['a pointer-to-func result naming a parameter run', 'func use() *func(run int) int {\n\t_ = run(1, 2)\n\treturn nil\n}', ['calls run L6']],
['a send-only channel-of-func result naming a parameter run', 'func use() chan<- func(run int) {\n\t_ = run(1, 2)\n\treturn nil\n}', ['calls run L6']],
['a func-type result naming a parameter run', 'func use() func(run int) int {\n\t_ = run(1, 2)\n\treturn nil\n}', ['calls run L6']],
])('keeps the edge for %s', async (_label, body, expected) => {
expect(await edges(mainGo(body, 'use()'), 'use')).toEqual(expected);
});
});