mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 23:20:31 +08:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user