mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 11:24:49 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user