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:
co-authored by
Claude Opus 4.8
parent
1f15f93feb
commit
dce61a5f4a
@@ -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 type’s methods; top-level bare hits functions only', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-swiftscope-'));
|
||||
fs.writeFileSync(
|
||||
|
||||
Reference in New Issue
Block a user