diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ae9fff5..ebc9a4471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 92907b0a3..747db167c 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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']); + }); }); }); diff --git a/__tests__/fixtures/kernel-parity/torture.kt b/__tests__/fixtures/kernel-parity/torture.kt index e993d5840..a916cc6d8 100644 --- a/__tests__/fixtures/kernel-parity/torture.kt +++ b/__tests__/fixtures/kernel-parity/torture.kt @@ -272,3 +272,9 @@ fun labeledLambda() { } fun whereClause(): Int where Int : Comparable = 1 + +class AccessorK { + val sameLineGetter: Int get() = compute() + var sameLinePair: Int get() = compute() + set(v) { draw(v) } +} diff --git a/codegraph-kernel/src/kotlin.rs b/codegraph-kernel/src/kotlin.rs index 6507affd4..b89969fe4 100644 --- a/codegraph-kernel/src/kotlin.rs +++ b/codegraph-kernel/src/kotlin.rs @@ -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> { let mut out: Vec> = Vec::new(); let mut after_eq = false; @@ -108,7 +110,7 @@ fn property_initializers<'t>(node: Node<'t>) -> Vec> { 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); } } diff --git a/src/extraction/languages/kotlin.ts b/src/extraction/languages/kotlin.ts index 5b79d2447..4ae71c966 100644 --- a/src/extraction/languages/kotlin.ts +++ b/src/extraction/languages/kotlin.ts @@ -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); } }