fix(extraction): land upstream declaration initializer walks (#1511) (#1802)

Squash danusha2345's PR #1511 at d282f9e8 onto main 8c9c4761,
preserving its nine non-merge commits and main's existing Unreleased notes.
Calls in Kotlin, Java, TS/JS, Scala, Rust and Python declaration initializers
now retain the owner established by the upstream regression expectations.
Include the upstream CFML, dynamic-dispatch summary and viewer follow-ups.

Linux fail-to-pass validation (Node 22.19.0, rebuilt dist and native kernel):
- Before: TS load belonged to file:app.ts; Python/Kotlin/Scala/Rust calls
  vanished; Java lost the field-lambda, anonymous override and eager calls.
- After: all six languages PASS; 12 native/WASM LF/CRLF parity checks PASS.
- Focused initializer regressions: 10 passed with CODEGRAPH_KERNEL=0 and
  10 passed with the kernel enabled; Kotlin's grammar fallback is recorded.
- Related regression suites: 879 passed, 1 skipped across 15 test files.
- Evidence: /workspace/cg-1510-repro/before and /workspace/cg-1510-repro/after
  (combined test output: after/vitest.log).

Fixes #1510
Supersedes #1511

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
This commit is contained in:
Colby Mchenry
2026-09-08 17:33:45 -05:00
committed by GitHub
co-authored by Colby McHenry danusha2345
parent 8c9c4761b0
commit 9181dd1ef3
25 changed files with 884 additions and 108 deletions
+266
View File
@@ -1094,6 +1094,42 @@ const token = getTokenMp();
);
expect(call).toBeDefined();
});
describe('initializer walk is scoped to the declared symbol (#693 for TS/JS)', () => {
const code = `
const eager = load();
const obj = { handler: () => target(), plain: target() };
const list = [() => target()];
export const exported = { handler: () => target() };
`;
const callersOf = (name: string) => {
const result = extractFromSource('app.ts', code);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
return result.unresolvedReferences
.filter((u) => u.referenceKind === 'calls' && u.referenceName === name)
.map((u) => byId.get(u.fromNodeId))
.map((n) => (n ? `${n.kind}:${n.name}` : '?'))
.sort();
};
it("a plain call initializer names the CONSTANT as caller, not the file", () => {
// The walk ran with only the file on the stack, so `load` recorded the
// file as its caller — useless for callers/impact.
expect(callersOf('load')).toEqual(['constant:eager']);
});
it('a non-exported object literal contributes calls (it was skipped outright)', () => {
// `exported`'s members are minted as their own function nodes, so its
// arrow's call comes from `handler`; the non-exported ones attribute to
// the declared constant.
expect(callersOf('target')).toEqual([
'constant:list',
'constant:obj',
'constant:obj',
'function:handler',
]);
});
});
});
describe('File Node Extraction', () => {
@@ -1182,6 +1218,42 @@ class UserService:
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('UserService');
});
it('walks a module-level assignment initializer scoped to the name (#693 for Python)', () => {
// The assignment minted a node and stopped, so everything a module builds
// at import time — `app = FastAPI()`, `ENGINE = create_engine(url)` — was
// missing from the graph. A tuple target mints no symbol, so its
// right-hand side attributes to the enclosing scope instead of vanishing.
const code = `
def target(): pass
def compute(): return 1
APP = compute()
handler = lambda: target()
MAPPING = {"a": compute()}
first, second = compute(), target()
class K:
ATTR = compute()
`;
const result = extractFromSource('app.py', code);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
const owners = result.unresolvedReferences
.filter((u) => u.referenceKind === 'calls')
.map((u) => {
const n = byId.get(u.fromNodeId);
return `${u.referenceName}<-${n ? `${n.kind}:${n.name}` : '?'}`;
})
.sort();
expect(owners).toEqual([
'compute<-class:K', // a class attribute still rides the class (no node of its own)
'compute<-file:app.py', // the tuple target mints nothing
'compute<-variable:APP',
'compute<-variable:MAPPING',
'target<-file:app.py',
'target<-variable:handler',
]);
});
});
describe('Go Extraction', () => {
@@ -1507,6 +1579,26 @@ impl Counter {
expect(implRefs).toHaveLength(0);
});
it('walks a const/static initializer scoped to the declared symbol (#693 for Rust)', () => {
// The declaration minted a node and stopped, so a handler table, a
// lazily-built singleton or any computed const linked to nothing.
const code = `
const LEN: usize = compute_len();
static REGISTRY: Lazy<Cfg> = Lazy::new(|| build_cfg());
`;
const result = extractFromSource('lib.rs', code);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
const owner = (name: string) => {
const u = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === name
);
const n = u ? byId.get(u.fromNodeId) : undefined;
return n ? `${n.kind}:${n.name}` : undefined;
};
expect(owner('compute_len')).toBe('variable:LEN');
expect(owner('build_cfg')).toBe('variable:REGISTRY');
});
it('should extract union declarations and their impl edges', () => {
const code = `
pub union Reg {
@@ -1714,6 +1806,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', () => {
@@ -2317,6 +2440,120 @@ class Bar {
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
expect(cls?.qualifiedName).toBe('Bar');
});
describe('property initializers are walked, attributed to the property (#693 for Kotlin)', () => {
// The property hook consumes the whole property_declaration subtree, so
// before this the initializer was only scanned for function-as-value
// candidates and every call inside it vanished from the graph. Android/MSDK
// callbacks are declared exactly this way (`private val l = Listener { … }`),
// so anything reached only through one looked like it had no callers at all.
const code = `
package repro
class Repro {
private val fieldLambda: () -> Unit = { target() }
private val samField = Runnable { target() }
private val plain = target()
private val delegated by lazy { target() }
private val anonObject = object : Runnable { override fun run() { target() } }
fun directCall() { target() }
fun lambdaInMethod() { run { target() } }
private fun target() {}
}
object Holder {
val topLevelLambda: () -> Unit = { hit() }
private fun hit() {}
}
`;
const callersOf = (target: string) => {
const result = extractFromSource('Repro.kt', code);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
return result.unresolvedReferences
.filter((u) => u.referenceKind === 'calls' && u.referenceName === target)
.map((u) => byId.get(u.fromNodeId)?.name)
.sort();
};
it('a lambda / SAM / plain / delegated / object initializer calls FROM the property', () => {
// `run` is the anonymous object's override, extracted as its own node
// under `anonObject` — the same shape Go's initializer walk produces.
expect(callersOf('target')).toEqual([
'delegated',
'directCall',
'fieldLambda',
'lambdaInMethod',
'plain',
'run',
'samField',
]);
});
it('a property in an `object` singleton is a caller too', () => {
expect(callersOf('hit')).toEqual(['topLevelLambda']);
});
it('an accessor body belongs to its property, written on either line', () => {
// `val x: T get() = …` nests the accessor UNDER the declaration; written
// on its own line the grammar makes it a following SIBLING instead. Both
// used to lose their calls (the nested one) or hand them to the enclosing
// class (the sibling); both now attribute to the property.
const src = `
package p
class C {
val sameLine: Int get() = compute()
val nextLine: Int
get() = compute()
var written: Int = 0
set(v) { store(v) }
private fun compute(): Int = 1
private fun store(v: Int) {}
}
`;
const result = extractFromSource('C.kt', src);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
const ownersOf = (name: string) =>
result.unresolvedReferences
.filter((u) => u.referenceKind === 'calls' && u.referenceName === name)
.map((u) => {
const n = byId.get(u.fromNodeId);
return n ? `${n.kind}:${n.name}` : '?';
})
.sort();
expect(ownersOf('compute')).toEqual(['field:nextLine', 'field:sameLine']);
expect(ownersOf('store')).toEqual(['field:written']);
});
it('an `init` block and a destructuring RHS no longer vanish', () => {
// Both mint no symbol of their own, so the hook consumed them and their
// code disappeared entirely; they now attribute to the enclosing scope.
const src = `
package p
class C {
init { val q = initCall() }
val (a, b) = makePair()
}
val (t1, t2) = topMakePair()
`;
const result = extractFromSource('C.kt', src);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
const owner = (name: string) => {
const u = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === name
);
const n = u ? byId.get(u.fromNodeId) : undefined;
return n ? `${n.kind}:${n.name}` : undefined;
};
expect(owner('initCall')).toBe('class:C');
expect(owner('makePair')).toBe('class:C');
expect(owner('topMakePair')).toBe('namespace:p');
});
});
});
describe('Dart Extraction', () => {
@@ -8294,6 +8531,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']);
});
});
});
@@ -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) {
@@ -74,6 +74,11 @@ export default {
},
};
// Initializer walks attributed to the declared symbol (#693). A plain call
// leaked to the FILE node; a non-exported object literal was skipped outright.
const eagerConfig = loadConfig();
const handlerMap = { onSave: () => persist(eagerConfig), onLoad: loadConfig() };
const lazyList = [() => persist(eagerConfig)];
// --- CommonJS export assignments (#1675) -----------------------------------
exports.getItems = async (req, res) => { res.json(await findItems()); };
module.exports.deleteItem = function (req, res) { removeItem(req.params.id); res.end(); };
@@ -50,6 +50,13 @@ val topDelegated by lazy { WidgetK(1) }
val (destA, destB) = makePair()
val withGetter: Int
get() = 42
val initLambda: () -> Unit = { caller() }
val initSam = Runnable { caller() }
val initObject = object : Runnable {
override fun run() {
caller()
}
}
class WidgetK(val size: Int, private var name: String = defaultName()) {
val area: Int = size * size
@@ -265,3 +272,20 @@ fun labeledLambda() {
}
fun whereClause(): Int where Int : Comparable<Int> = 1
class AccessorK {
val sameLineGetter: Int get() = compute()
var sameLinePair: Int get() = compute()
set(v) { draw(v) }
}
class SiblingAccessorK {
var nextLine: Int = 0
get() = compute()
set(v) { draw(v) }
val (localA, localB) = makePair()
init {
val fromInit = compute()
register(fromInit)
}
}
@@ -48,6 +48,11 @@ def shadowed():
handlers = {"recv": target_cb}
callbacks = [target_cb, view]
# Initializer walks attributed to the assigned name (#693).
INIT_EAGER = helper()
INIT_LAMBDA = lambda: target_cb()
INIT_MAP = {"a": helper()}
init_a, init_b = helper(), view()
# --- call receivers (#1683) ---------------------------------------------------
def bucket_chains(d, k, v):
@@ -285,6 +285,11 @@ fn mount() {
routes![top_level_h];
// Initializer walks attributed to the declared symbol (#693).
const INIT_CONST: usize = compute_len();
static INIT_LAZY: Lazy<Cfg> = Lazy::new(|| build_cfg());
static INIT_ALIAS: fn() = free_fn;
pub union Reg {
pub raw: u32,
pub halves: [u16; 2],
@@ -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() }
}
+3 -1
View File
@@ -795,8 +795,10 @@ describe('Function-as-value capture (#756)', () => {
// The DRF wiring: get_serializer_class → the imported serializer class,
// via `return` — the issue's headline gap. The module-level registry
// dict rides the file node.
// dict rides BOTH the assigned name (the initializer walk, #693) and the
// file node (the dispatcher's own scan, which runs either way).
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerFull'))).toEqual([
'SERIALIZER_REGISTRY',
'get_serializer_class',
'views.py',
]);
+3 -1
View File
@@ -5,7 +5,9 @@
* compiled from the vendored fwcd 0.3.8 C sources, the arc's first
* vendored-grammar-C language) produces the SAME ExtractionResult as the
* wasm TreeSitterExtractor over the checked-in torture fixture (torture.kt:
* the property hook's scope classification, extension-function receiver QNs
* the property hook's scope classification and its initializer walk (a
* lambda / SAM / anonymous-object RHS attributing its calls to the property),
* extension-function receiver QNs
* (`WidgetK::extend`, the qualified `com::qext` bug) + the owner-contains
* fallback, expect/actual → node DECORATORS (the KMP synthesizer feed),
* the bodiless-vs-bodied class header asymmetry, comment-glued