fix(scala): walk a val/var initializer scoped to the declared symbol (#693 for Scala)

The val/var hook minted the node and returned true, so the dispatcher only
scanned the subtree for function-as-value candidates and the initializer's
code was never walked:

    val fieldLambda: () => Unit = () => target()   // no caller
    val direct = target()                          // no caller
    lazy val lazily = compute()                    // no caller
    val anon = new Runnable { def run() = target() }  // no caller

Scala puts almost everything in a val, so this is not an edge case: on a
32-file SpinalHDL project the graph was missing 812 references.

Fix: walk the `value` field through visitFunctionBody with the declared symbol
pushed on the scope stack — the same shape as Go's #693/#744 fix, which the
grammar supports directly here (`val_definition` exposes `value`).

Kernel and TS extractor move together; kernel parity holds.

Measured on that SpinalHDL project: 0 lost refs, 0 re-attributed, +812 new,
node and edge counts unchanged (824/792), 32/32 files byte-parity between the
two arms.
This commit is contained in:
danusha2345
2026-08-05 23:13:23 +03:00
parent 8b2e43613c
commit a86703bc79
5 changed files with 59 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)
- Scala `val`/`var` definitions now contribute the calls their initializer makes — `val handler = () => process(msg)`, `val client = buildClient()`, `lazy val engine = start()`. Previously everything on the right-hand side was dropped, which on a val-heavy codebase (SpinalHDL hardware descriptions, Akka wiring) is most of the wiring: a 32-file SpinalHDL project gained 812 references it had been missing. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
- 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.
+29
View File
@@ -7710,6 +7710,35 @@ def processData(): Unit = {
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
expect(calls.length).toBeGreaterThan(0);
});
it('walks a val/var initializer scoped to the declared symbol (#693 for Scala)', () => {
// The val/var hook minted the node and returned true, so the dispatcher
// only scanned the subtree for function-as-value candidates — every call
// in an initializer was dropped, which on a `val`-heavy codebase
// (SpinalHDL, Akka wiring) is most of the wiring.
const code = `
class C {
val fieldLambda: () => Unit = () => target()
val direct = target()
lazy val lazily = target()
private def target(): Unit = {}
}
object O {
val topLambda = () => hit()
def hit(): Unit = {}
}
`;
const result = extractFromSource('C.scala', 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();
expect(callersOf('target')).toEqual(['direct', 'fieldLambda', 'lazily']);
expect(callersOf('hit')).toEqual(['topLambda']);
});
});
});
@@ -175,3 +175,10 @@ package object utilpkg {
def pkgHelper(): Int = 1
val pkgShared = 2
}
class InitWalk {
val initLambda: () => Unit = () => helperCall()
val initDirect = helperCall()
lazy val initLazy = process(1)
val initAnon = new Runnable { def run(): Unit = helperCall() }
}
+12
View File
@@ -652,6 +652,18 @@ impl<'t> Walker<'t> {
if let (Some(row), Some(t)) = (created, type_node) {
self.emit_scala_type_refs(t, row);
}
// Walk the initializer ATTRIBUTED to the declared symbol
// (#693, the Go fix): the hook consumes this subtree and the
// dispatcher only fn-ref-scans it, so `val cb = () => target()`
// — and even a plain `val x = compute()` — emitted no call edge
// at all.
if let Some(row) = created {
if let Some(value) = node.child_by_field_name("value") {
self.stack.push(Scope { row, kind, name: name.clone() });
self.visit_body(value);
self.stack.pop();
}
}
true
}
"enum_case_definitions" => {
+10
View File
@@ -166,6 +166,16 @@ export const scalaExtractor: LanguageExtractor = {
const created = ctx.createNode(kind, name, node, { signature: sig, visibility: extractVisibility(node) });
if (created && typeNode) emitScalaTypeRefs(typeNode, created.id, ctx, ctx.source);
// Walk the initializer ATTRIBUTED to the declared symbol (#693, the Go
// fix): the hook consumes this subtree and the dispatcher only scans it
// for function-as-value candidates, so `val cb = () => target()` — and
// even a plain `val x = compute()` — emitted no call edge at all.
const valueNode = node.childForFieldName('value');
if (created && valueNode) {
ctx.pushScope(created.id);
ctx.visitFunctionBody(valueNode, created.id);
ctx.popScope();
}
return true;
}