fix(kotlin): walk a same-line property accessor too

Follow-up to cf582f4. `val url: String get() = build(host)` nests the accessor
UNDER the property_declaration, so the hook consumed it and everything the
getter body called vanished — the initializer walk deliberately skipped
`getter`/`setter` on the theory that they are declaration, not code. They are
code.

Measured on the Android project this arc is validated against: 16 such
properties, whose accessor bodies contributed nothing at all.

An accessor written on its OWN line parses as a SIBLING of the property, not a
child, so it stays out of reach here and keeps attributing to the enclosing
class exactly as before. That asymmetry is the grammar's, and closing it means
looking backwards from a sibling accessor to the preceding declaration —
separate change, separate risk.

Whole-project re-measure (110 Kotlin + 100 JS + 37 Go files, both arms of the
extractor): 0 genuinely lost edges, +142 edges. The 13 edges that changed
identity are 9 JS initializer calls moving off the file node onto their
declaring constant, and 4 Kotlin `run(…)` call sites that used to name-match a
JavaScript test helper in desktop/test/ and now match a Kotlin `run` override.
This commit is contained in:
danusha2345
2026-08-05 23:08:33 +03:00
parent a10a7fa6f7
commit 8b2e43613c
5 changed files with 54 additions and 16 deletions
+1
View File
@@ -28,6 +28,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- TypeScript and JavaScript module-level declarations now name themselves as the caller of whatever their initializer runs. `const cfg = loadConfig()` recorded the *file* as loadConfig's caller, which is no use for callers or impact; it now records `cfg`. And an object literal that wasn't exported — `const handlers = { onSave: () => persist() }` — was skipped entirely, so nothing inside it reached the graph at all. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
- `codegraph_explore` again lists a dynamic-dispatch link when the same two symbols are also joined by an ordinary call, instead of dropping it from the summary.
- Java fields initialized with a lambda or an anonymous class — `private final Runnable r = () -> doWork();`, `new LocationListener() { … }`, `Parcelable.Creator` — now contribute call edges, and the anonymous class and its overrides become real symbols instead of being invisible. Previously everything inside a field initializer was dropped, so a method reached only from one looked like it had no callers. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
- A Kotlin property whose accessor is written on the same line — `val url: String get() = build(host)` — now contributes its accessor's calls too, instead of dropping them.
- Kotlin properties that hold a lambda, a SAM callback or an anonymous object — `private val frameListener = CameraFrameListener { … }`, the way Android and MSDK callbacks are almost always declared — now contribute call edges. Previously everything inside such an initializer was dropped, so a function reached only through one of these callbacks looked like it had no callers at all and its blast radius came back far too small. Delegated properties (`by lazy { … }`) and plain initializers (`val x = compute()`) were affected the same way and are fixed too. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
- The blast-radius section of `codegraph_explore` flagged "no covering tests found" whenever no test called a symbol directly — falsely branding helpers that tests exercise through their callers as untested (about 40% of flagged symbols in a measured sample). The check now follows caller chains up to 3 hops and reports indirect coverage as "tested via callers"; when nothing is found it states exactly what was checked instead of an unconditional warning. Thanks @inth3shadows for measuring the false-positive rate. (#1475)
+27
View File
@@ -2061,6 +2061,33 @@ object Holder {
it('a property in an `object` singleton is a caller too', () => {
expect(callersOf('hit')).toEqual(['topLevelLambda']);
});
it('a same-line accessor body is walked too; a next-line one still is not', () => {
// `val x: T get() = …` nests the accessor UNDER the declaration, so the
// hook consumed it and its calls vanished. Written on its own line the
// accessor parses as a SIBLING of the property — out of reach from here,
// and still attributed to the enclosing class as before.
const src = `
package p
class C {
val sameLine: Int get() = compute()
val nextLine: Int
get() = compute()
private fun compute(): Int = 1
}
`;
const result = extractFromSource('C.kt', src);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
const owners = result.unresolvedReferences
.filter((u) => u.referenceKind === 'calls' && u.referenceName === 'compute')
.map((u) => {
const n = byId.get(u.fromNodeId);
return n ? `${n.kind}:${n.name}` : '?';
})
.sort();
expect(owners).toEqual(['class:C', 'field:sameLine']);
});
});
});
@@ -272,3 +272,9 @@ fun labeledLambda() {
}
fun whereClause(): Int where Int : Comparable<Int> = 1
class AccessorK {
val sameLineGetter: Int get() = compute()
var sameLinePair: Int get() = compute()
set(v) { draw(v) }
}
+10 -8
View File
@@ -87,13 +87,15 @@ fn strip_js_ws(s: &str) -> String {
s.chars().filter(|c| !is_js_space(*c)).collect()
}
/// A property's RHS: the named child right after the `=` token, plus a
/// `property_delegate` (`by lazy { … }`). Everything else a
/// `property_declaration` can hold is a declaration, not code — modifiers, the
/// `val`/`var` keyword, the name+type, an extension receiver's type and type
/// parameters, and `getter`/`setter` — so it stays unwalked. (Go's #693 fix
/// walks the `value` field for the same reason; this grammar exposes no fields
/// at all, hence the `=` anchor.)
/// A property's CODE children: the named child right after the `=` token, a
/// `property_delegate` (`by lazy { … }`), and an accessor the grammar nested
/// under the declaration (`val x: Int get() = compute()` — written on ONE line;
/// an accessor on its own line parses as a SIBLING of the property and is not
/// reachable from here). What stays unwalked is the declaration itself —
/// modifiers, the `val`/`var` keyword, the name+type, and an extension
/// receiver's type and type parameters. (Go's #693 fix walks the `value` field
/// for the same reason; this grammar exposes no fields at all, hence the `=`
/// anchor.)
fn property_initializers<'t>(node: Node<'t>) -> Vec<Node<'t>> {
let mut out: Vec<Node<'t>> = Vec::new();
let mut after_eq = false;
@@ -108,7 +110,7 @@ fn property_initializers<'t>(node: Node<'t>) -> Vec<Node<'t>> {
if after_eq {
out.push(c);
after_eq = false;
} else if c.kind() == "property_delegate" {
} else if matches!(c.kind(), "property_delegate" | "getter" | "setter") {
out.push(c);
}
}
+10 -8
View File
@@ -43,13 +43,15 @@ function extractKotlinReturnType(node: SyntaxNode, source: string): string | und
}
/**
* A property's RHS: the named child right after the `=` token, plus a
* `property_delegate` (`by lazy { … }`). Everything else a
* `property_declaration` can hold is a declaration, not code — modifiers, the
* `val`/`var` keyword, the name+type, an extension receiver's type and type
* parameters, and `getter`/`setter` — so it stays unwalked. (Go's #693 fix
* walks the `value` field for the same reason; tree-sitter-kotlin exposes no
* fields at all, hence the `=` anchor.)
* A property's CODE children: the named child right after the `=` token, a
* `property_delegate` (`by lazy { … }`), and an accessor the grammar nested
* under the declaration (`val x: Int get() = compute()` — written on ONE line;
* an accessor on its own line parses as a SIBLING of the property and is not
* reachable from here). What stays unwalked is the declaration itself —
* modifiers, the `val`/`var` keyword, the name+type, and an extension
* receiver's type and type parameters. (Go's #693 fix walks the `value` field
* for the same reason; tree-sitter-kotlin exposes no fields at all, hence the
* `=` anchor.)
*/
function kotlinPropertyInitializers(node: SyntaxNode): SyntaxNode[] {
const out: SyntaxNode[] = [];
@@ -64,7 +66,7 @@ function kotlinPropertyInitializers(node: SyntaxNode): SyntaxNode[] {
if (afterEq) {
out.push(c);
afterEq = false;
} else if (c.type === 'property_delegate') {
} else if (c.type === 'property_delegate' || c.type === 'getter' || c.type === 'setter') {
out.push(c);
}
}