fix(extraction): qualified Type::member refs skip the name gate — no-import references resolve (#812)

`KtHandlers::handle` registered from another file produced no edge: the
extraction gate required the scope to be a same-file type or an IMPORTED
name, but Java/Kotlin same-package references and Kotlin companion members
need no import at all, so the gate could never see them. (The "companion
members extract unqualified" limit recorded during Arc A was a probe
artifact: a SINGLE-LINE `class X { companion object { … } }` is an
upstream tree-sitter-kotlin misparse (ERROR node); real multi-line
companions extract transparently as qualified methods of the class.)

Qualified `Type::member` candidates now skip the name gate the same way
`this.<member>` ones do: the explicit-ref syntax is self-selecting, and
resolution stays scope-suffix-anchored + unique-or-drop, so a
`Decoy::handle` can never match a `KtHandlers::handle` ref (tested).

A/B vs main: rxjava +4 (same-package `Maybe::just` / `Single::just`
method refs), fmt +3 (gtest `&Test::DeleteSelf_` /
`&TestSuite::RunSetUpTestSuite` cross-file member pointers), okio 0-delta,
redis byte-identical — every new edge verified genuine, zero calls edges
touched, node counts identical.

Full suite 1392 passed. EXTRACTION_VERSION 22 → 23 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 15:44:14 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 1f15f93feb
commit dce61a5f4a
5 changed files with 65 additions and 31 deletions
+45
View File
@@ -544,6 +544,51 @@ describe('Function-as-value capture (#756)', () => {
}
});
it('KOTLIN: companion-object refs resolve cross-file without imports; decoy companion untouched', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-ktcomp-'));
// Same package, no imports — the Java/Kotlin reality the name gate can't
// see, which is why qualified `Type::member` candidates skip it.
fs.writeFileSync(
path.join(tmpDir, 'Handlers.kt'),
[
'class KtHandlers {',
' companion object {',
' fun handle(x: Int) {}',
' }',
'}',
'class Decoy {',
' companion object {',
' fun handle(x: Int) {}',
' }',
'}',
].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, 'Wirer.kt'),
[
'fun register(cb: Any) {}',
'class Wirer {',
' fun wire() { register(KtHandlers::handle) }',
'}',
].join('\n')
);
const cg = CodeGraph.initSync(tmpDir);
try {
await cg.indexAll();
const handles = cg.getNodesByName('handle');
const target = handles.find((n) => n.qualifiedName.includes('KtHandlers'))!;
const decoy = handles.find((n) => n.qualifiedName.includes('Decoy'))!;
const into = cg.getIncomingEdges(target.id).filter((e) => e.metadata?.fnRef === true);
expect(into).toHaveLength(1);
expect(cg.getNode(into[0]!.source)?.name).toBe('wire');
expect(cg.getIncomingEdges(decoy.id).filter((e) => e.metadata?.fnRef === true)).toHaveLength(0);
} finally {
cg.destroy();
tmpDir = undefined;
}
});
it('SWIFT SCOPING: bare ids hit only the enclosing types methods; top-level bare hits functions only', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-swiftscope-'));
fs.writeFileSync(