A Kotlin method called through a companion-object factory, fluent chain, or
constructor — `Foo.getInstance().bar()`, `Config.create(opts).build()`,
`STMTransaction(f).commit()` — dropped the receiver to a BARE method name, which
then name-matched a same-named method on an unrelated class (a wrong edge) or
failed to resolve. Ports the #645/#608 mechanism to Kotlin:
- Part 1: capture Kotlin return types in the extractor. tree-sitter-kotlin
exposes no field names, so the return type is read positionally (the type node
after function_value_parameters); inferred/Unit/Nothing returns yield none.
- Part 2: encode a CLASS/companion-factory call-receiver chain as `inner().method`.
Gated to a capitalized receiver (`Foo.getInstance()` / `Foo(args)`) so instance
chains (`list.filter{}.map{}`) keep their bare-name behavior — re-encoding those
would only drop the edge, regressing recall in fluent codebases.
- Part 3: generalize matchJavaCallChain -> matchDottedCallChain (shared by the JVM
dot-notation languages); resolve the method on the factory's return type, or on
the constructed class for a Kotlin `Foo(args).method()` receiver. Validated via
resolveMethodOnType, so a wrong inference yields NO edge.
Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on arrow-kt/arrow (734 .kt) — node count identical (no explosion),
+49 validated-correct chained edges, and the removed edges are wrong bare-name
guesses the fix correctly stops emitting (419/438 from test/doc files; the 18
from product code are stdlib `.apply{}`, self-loops, and bare-name mismatches) —
a net precision improvement, ~0 correct product edges lost. Java path unchanged
(constructor branch is Kotlin-gated). EXTRACTION_VERSION 6 -> 7.
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
7f6bdf7ad1
commit
3e04650850
@@ -2256,6 +2256,79 @@ class Other { void onlyOther() {} }
|
||||
class Caller {
|
||||
void run() { Foo.getInstance().onlyOther(); }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// Foo has no onlyOther() — must not mis-attach to the same-named Other::onlyOther.
|
||||
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kotlin chained companion-factory call resolution (#645/#608 mechanism)', () => {
|
||||
function callerNamesOf(qualifiedName: string): string[] {
|
||||
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||
if (!target) return [];
|
||||
const names = cg
|
||||
.getIncomingEdges(target.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.source)?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
return [...new Set(names)].sort();
|
||||
}
|
||||
|
||||
it('resolves Foo.getInstance().bar() via the companion return type, never a same-named decoy', async () => {
|
||||
// Aaa sorts first and has a same-named bar() — without the chain fix Kotlin
|
||||
// dropped the receiver to a bare `bar` and attached to Aaa (a wrong edge).
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'Main.kt'),
|
||||
`class Aaa { fun bar() {} }
|
||||
class Foo {
|
||||
companion object {
|
||||
fun getInstance(): Foo = Foo()
|
||||
}
|
||||
fun bar() {}
|
||||
}
|
||||
class Caller {
|
||||
fun run() { Foo.getInstance().bar() }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Foo::bar')).toEqual(['run']);
|
||||
expect(callerNamesOf('Aaa::bar')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a companion factory chain that passes arguments — Foo.create(cfg).build()', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'Main.kt'),
|
||||
`class Config
|
||||
class Foo {
|
||||
companion object {
|
||||
fun create(c: Config): Foo = Foo()
|
||||
}
|
||||
fun build() {}
|
||||
}
|
||||
class Caller {
|
||||
fun run() { Foo.create(Config()).build() }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Foo::build')).toEqual(['run']);
|
||||
});
|
||||
|
||||
it('creates NO edge when the companion return type lacks the method (silent miss, not a wrong edge)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'Main.kt'),
|
||||
`class Foo {
|
||||
companion object {
|
||||
fun getInstance(): Foo = Foo()
|
||||
}
|
||||
}
|
||||
class Other { fun onlyOther() {} }
|
||||
class Caller {
|
||||
fun run() { Foo.getInstance().onlyOther() }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
Reference in New Issue
Block a user