From 468f623745d1260c74615b4426413756d0f6a899 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Wed, 5 Aug 2026 22:26:11 +0300 Subject: [PATCH] fix(java): walk a field's initializer scoped to the field (#693 for Java) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `extractField` minted the field node and stopped; the dispatcher then only scanned the `field_declaration` subtree for function-as-value candidates. So a field initializer's code was never walked: private final Runnable fieldLambda = () -> target(); // no caller private final Runnable l = new LocationListener() { … }; // invisible private final int eager = compute(); // no caller The anonymous-class form lost more than edges — the class and its overrides were never extracted at all, which is how `Parcelable.Creator` and every Android listener field is written. Fix: walk the declarator's `value` through visitFunctionBody with the field pushed on the scope stack — Go's #693/#744 fix, and the same shape as the TS/JS class-field walk already sitting in the methodTypes branch (#808). `extractField` is shared, but the walk is keyed on the `value` FIELD, which only Java's `variable_declarator` carries: C# (bare child), VB.NET (`initializer`) and PHP (`default_value`, separate branch) are untouched and stay for their own turn. Kernel and TS extractor move together, so kernel parity stays byte-identical. Measured on the 409-file DJI MSDK v5 UX SDK (Java): strictly additive, 0 lost nodes / edges / refs, +21 nodes (anonymous listener and Creator classes with their overrides), +21 edges, +651 refs, 409/409 files still byte-parity between the two arms. --- CHANGELOG.md | 1 + __tests__/extraction.test.ts | 31 +++++++++++++++++++ __tests__/fixtures/kernel-parity/Torture.java | 9 ++++++ codegraph-kernel/src/java.rs | 10 ++++++ src/extraction/tree-sitter.ts | 15 +++++++++ 5 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec097c90..2b9d9f094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - On Windows, the Claude Code prompt hook written by `codegraph install` failed with "command not found" when hooks run through Git Bash, which needs the `.cmd` extension to find the launcher. The installer now writes the platform-correct command, and re-running `codegraph install` (or `codegraph upgrade`) repairs an existing install in place. (#1466) - Python classes used as values — `return SomeSerializer` from a factory method, `handler = SomeClass` aliases, registry dicts and lists, and classes passed as arguments — now produce reference edges in the graph. Previously these idioms were invisible, so on Django and Django REST Framework projects, asking for a serializer's callers or the impact of editing it missed the views that actually use it. Re-index after upgrading to pick up the new edges. (#1478) - When a file changed on disk after its last index sync, `codegraph_node` and `codegraph_explore` could return a different symbol's code under the requested name — current file bytes cut at outdated line positions — while presenting it as verbatim, trustworthy source. This hit hardest on projects queried through `projectPath` (for example, sub-projects of a monorepo), which have no live file watcher to flag pending edits. Both tools now verify each file against the index before showing sliced code: an out-of-date file is either shown whole with its full current source, or its code is withheld with a clear "changed on disk" notice — never served as a wrong slice. A fresh re-index restores normal output automatically. Thanks @inth3shadows for the thorough report and verification passes. (#1474) +- 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. - 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 15cff153a..31827e8fe 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -1337,6 +1337,37 @@ public class Splitter { ); expect(sepStart, 'override inside the lambda-returned anon class should be a method node').toBeDefined(); }); + + it('walks a field initializer scoped to the field (#693 for Java)', () => { + // The dispatcher only scanned a field_declaration for function-as-value + // candidates, so a lambda or anonymous class holding the work — the + // Android listener idiom — contributed no call edge and `target` looked + // callerless. + const code = ` +package p; +class T { + private final Runnable fieldLambda = () -> target(); + private final Runnable anonClass = new Runnable() { + public void run() { target(); } + }; + private final int eager = compute(); + void directCall() { target(); } + private void target() {} + private static int compute() { return 1; } +} +`; + const result = extractFromSource('T.java', code); + const byId = new Map(result.nodes.map((n) => [n.id, n])); + const callersOf = (name: string) => + result.unresolvedReferences + .filter((u) => u.referenceKind === 'calls' && u.referenceName === name) + .map((u) => byId.get(u.fromNodeId)?.name) + .sort(); + + // `run` is the anonymous class's override, itself extracted under the field. + expect(callersOf('target')).toEqual(['directCall', 'fieldLambda', 'run']); + expect(callersOf('compute')).toEqual(['eager']); + }); }); describe('C# Extraction', () => { diff --git a/__tests__/fixtures/kernel-parity/Torture.java b/__tests__/fixtures/kernel-parity/Torture.java index 703dc3b3c..9ecb8d823 100644 --- a/__tests__/fixtures/kernel-parity/Torture.java +++ b/__tests__/fixtures/kernel-parity/Torture.java @@ -22,6 +22,15 @@ public class TortureService extends BaseService implements Runnable, AutoCloseab protected int count = 0; private final List names; int packagePrivate, secondDeclarator; + /** Field initializers — walked scoped to the field (#693). */ + private final Runnable fieldLambda = () -> helper(RETRY_LIMITS); + private final Runnable fieldAnonClass = new Runnable() { + @Override + public void run() { + helper(RETRY_LIMITS); + } + }; + private final Runnable fieldMethodRef = TortureService::compute; /** Ctor javadoc. */ public TortureService(List names) { diff --git a/codegraph-kernel/src/java.rs b/codegraph-kernel/src/java.rs index d2017a093..da789b7b3 100644 --- a/codegraph-kernel/src/java.rs +++ b/codegraph-kernel/src/java.rs @@ -751,6 +751,16 @@ impl<'t> Walker<'t> { if let Some(row) = row { self.extract_decorators_for(node, row); self.extract_type_annotations(node, row); + // Walk the initializer ATTRIBUTED to the declared field + // (#693, the Go fix): the dispatcher only fn-ref-scans this + // subtree, so a lambda / method reference / anonymous class + // in `private final Runnable r = () -> target();` emitted no + // call edge at all. + if let Some(value) = decl.child_by_field_name("value") { + self.stack.push(Scope { row, kind: field_kind, name: name.clone() }); + self.visit_function_body(value); + self.stack.pop(); + } } } } else { diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 9e53e62da..dfb3b3e26 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -2143,6 +2143,21 @@ export class TreeSitterExtractor { // and the language-aware path in `extractTypeAnnotations` descends // into that wrapper (#381). this.extractTypeAnnotations(node, fieldNode.id); + // Walk the initializer ATTRIBUTED to the declared field (#693, the + // Go fix; same shape as the TS/JS class-field walk above). The + // dispatcher only scanned this subtree for function-as-value + // candidates, so a lambda / method reference / anonymous class in + // `private final Runnable r = () -> target();` contributed NO call + // edge at all and `target` looked callerless. Keyed on the `value` + // FIELD, which only Java's `variable_declarator` carries — C#, + // VB.NET and PHP spell their initializer differently and are + // deliberately untouched here. + const valueNode = getChildByField(decl, 'value'); + if (valueNode) { + this.nodeStack.push(fieldNode.id); + this.visitFunctionBody(valueNode, fieldNode.id); + this.nodeStack.pop(); + } } } } else {