fix(extraction): a function in an initializer is referenced once, by what the initializer declares (#2743)

PRs #693 and #1802 walk a declaration's initializer under the symbol it declares,
so `const routes = { home }` references `home` from `routes`. The
dispatcher's declaration-wide function-as-value scan then took the same
values again for the enclosing file (or class). Flush-time dedup keys on
the source, so both stayed: a second reference to the same function from
the same spot.

Each initializer walk now records what it walked, and the scan skips it.
PR #2738 added that mechanism for component wrappers, and C#, Scala and Dart
already had their own. The newly covered paths:
- TS/JS module-level constants and variables, owned object literals
  included (#2300), and TS/JS class properties;
- Python assignments, Go var/const specs, Rust const/static items;
- Java fields, and Kotlin properties (walkInitializer takes the property's
  id, so their walk is unchanged).
Mirrored in the kernel: tsjs, python, go, rustlang, java and kotlin.

References no initializer walk takes are unchanged: a module-level call
argument `register(fn)`, C-family tables, Rust's `static CB: fn() = handler`
(the declaration's own value, which the scan still reads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 11:41:45 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ae0fc631e0
commit 51be28d15b
17 changed files with 233 additions and 79 deletions
+70 -3
View File
@@ -779,12 +779,11 @@ describe('Function-as-value capture (#756)', () => {
// The DRF wiring: get_serializer_class → the imported serializer class,
// via `return` — the issue's headline gap. The module-level registry
// dict rides BOTH the assigned name (the initializer walk, #693) and the
// file node (the dispatcher's own scan, which runs either way).
// dict rides the assigned name (the initializer walk, #693), and not the
// file node as well: the dispatcher's scan skips what that walk took.
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerFull'))).toEqual([
'SERIALIZER_REGISTRY',
'get_serializer_class',
'views.py',
]);
// Second branch return + a module-level call argument.
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerBrief'))).toEqual([
@@ -1581,4 +1580,72 @@ func Unknown(obj interface{}) { Submit(obj.Fetch) }
cg.destroy();
}
});
// An initializer walked under the symbol it declares (#693) takes its
// function values for that symbol. The declaration-wide scan took each one
// again for the enclosing file or class: a second reference to the same
// function from the same spot (TS/JS constants and class fields, Python,
// Go, Rust, Java fields, Kotlin properties). Each function here is
// referenced only from an initializer, so a second source shows.
it('INITIALIZER WALKS: a function in an initializer is referenced once, by what it declares', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-init-'));
fs.writeFileSync(
path.join(tmpDir, 'app.ts'),
[
'export function tsHome() {}',
'export function tsAbout() {}',
'export function tsClick() {}',
'export function tsBoot() {}',
'function register(fn: () => void) {}',
'const routes = { tsHome, about: tsAbout, list: [tsHome] };',
'const api = { load() {}, tsAbout };',
'export class Widget {',
' static handlers = { click: tsClick };',
'}',
'register(tsBoot);',
].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, 'views.py'),
['def py_home():', ' pass', '', 'HANDLERS = {"home": py_home}', 'ROUTES = [py_home]'].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, 'handlers.go'),
['package main', '', 'func goHome() {}', '', 'var handlers = map[string]func(){"home": goHome}'].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, 'lib.rs'),
['fn rust_home() {}', 'static HANDLERS: &[fn()] = &[rust_home];'].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, 'Registry.java'),
[
'class Registry {',
' static void javaHome() {}',
' static final java.util.List<Runnable> TABLE = java.util.List.of(Registry::javaHome);',
'}',
].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, 'handlers.kt'),
['fun ktHome() {}', 'val ktHandlers = listOf(::ktHome)', 'class KtWidget {', ' val hooks = listOf(::ktHome)', '}'].join('\n')
);
const cg = CodeGraph.initSync(tmpDir);
try {
await cg.indexAll();
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsHome'))).toEqual(['routes']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsAbout'))).toEqual(['api', 'routes']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsClick'))).toEqual(['handlers']);
// A call argument at module scope is the file's, as before.
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsBoot'))).toEqual(['app.ts']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'py_home'))).toEqual(['HANDLERS', 'ROUTES']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'goHome'))).toEqual(['handlers']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'rust_home'))).toEqual(['HANDLERS']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'javaHome'))).toEqual(['TABLE']);
expect(sourceNames(cg, fnRefEdgesInto(cg, 'ktHome'))).toEqual(['hooks', 'ktHandlers']);
} finally {
cg.destroy();
}
});
});