fix(java): walk a field's initializer scoped to the field (#693 for Java)

`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.
This commit is contained in:
danusha2345
2026-08-05 22:26:11 +03:00
parent cf582f4274
commit 468f623745
5 changed files with 66 additions and 0 deletions
+1
View File
@@ -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)
+31
View File
@@ -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', () => {
@@ -22,6 +22,15 @@ public class TortureService extends BaseService implements Runnable, AutoCloseab
protected int count = 0;
private final List<String> 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<String> names) {
+10
View File
@@ -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 {
+15
View File
@@ -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 {