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:
co-authored by
Colby McHenry
danusha2345
parent
8c9c4761b0
commit
9181dd1ef3
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user