From a86703bc798d004a2be1a864c1867bea20a6e434 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Wed, 5 Aug 2026 23:13:23 +0300 Subject: [PATCH] fix(scala): walk a val/var initializer scoped to the declared symbol (#693 for Scala) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 1 + __tests__/extraction.test.ts | 29 +++++++++++++++++++ .../fixtures/kernel-parity/torture.scala | 7 +++++ codegraph-kernel/src/scala.rs | 12 ++++++++ src/extraction/languages/scala.ts | 10 +++++++ 5 files changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc9a4471..7d6dc8d3f 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) +- 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. diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 747db167c..68940ee06 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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']); + }); }); }); diff --git a/__tests__/fixtures/kernel-parity/torture.scala b/__tests__/fixtures/kernel-parity/torture.scala index de536e659..c1e4703ae 100644 --- a/__tests__/fixtures/kernel-parity/torture.scala +++ b/__tests__/fixtures/kernel-parity/torture.scala @@ -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() } +} diff --git a/codegraph-kernel/src/scala.rs b/codegraph-kernel/src/scala.rs index eb7a0bd3d..01aa36bf7 100644 --- a/codegraph-kernel/src/scala.rs +++ b/codegraph-kernel/src/scala.rs @@ -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" => { diff --git a/src/extraction/languages/scala.ts b/src/extraction/languages/scala.ts index 7e05daf66..b0d995faa 100644 --- a/src/extraction/languages/scala.ts +++ b/src/extraction/languages/scala.ts @@ -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; }