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
+140 -25
View File
@@ -42,6 +42,99 @@ function extractKotlinReturnType(node: SyntaxNode, source: string): string | und
return undefined;
}
/**
* A property's CODE children: the named child right after the `=` token, a
* `property_delegate` (`by lazy { … }`), and an accessor the grammar nested
* under the declaration (`val x: Int get() = compute()` — written on ONE line;
* an accessor on its own line parses as a SIBLING of the property and is not
* reachable from here). What stays unwalked is the declaration itself —
* modifiers, the `val`/`var` keyword, the name+type, and an extension
* receiver's type and type parameters. (Go's #693 fix walks the `value` field
* for the same reason; tree-sitter-kotlin exposes no fields at all, hence the
* `=` anchor.)
*/
function kotlinPropertyInitializers(node: SyntaxNode): SyntaxNode[] {
const out: SyntaxNode[] = [];
let afterEq = false;
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (!c) continue;
if (!c.isNamed) {
if (c.type === '=') afterEq = true;
continue;
}
if (afterEq) {
out.push(c);
afterEq = false;
} else if (c.type === 'property_delegate' || c.type === 'getter' || c.type === 'setter') {
out.push(c);
}
}
return out;
}
/**
* A property's node kind, or null when the declaration mints no node at all:
* destructuring (`val (a, b) = …`), an unreadable name, or a local (one inside
* a function body / `init` block / lambda / accessor). Kind by enclosing scope:
* a singleton `object` / `companion object` — and a top-level property — holds
* *shared* values, so `val`→`constant` and `var`→`variable` (the Scala-object
* rule; a `const val` is just a val). A `class`/`interface`/`enum` instance
* `val`/`var` is per-instance state → `field` (never a value-ref target, like a
* Java instance `final`).
*/
function kotlinPropertyKind(
node: SyntaxNode,
source: string
): 'field' | 'constant' | 'variable' | null {
const varDecl = node.namedChildren.find((c) => c.type === 'variable_declaration');
const nameNode = varDecl?.namedChildren.find((c) => c.type === 'simple_identifier');
if (!nameNode || !getNodeText(nameNode, source)) return null;
let scope: 'local' | 'const' | 'instance' = 'const';
for (let p = node.parent; p; p = p.parent) {
const pt = p.type;
if (
pt === 'function_body' || pt === 'function_declaration' ||
pt === 'lambda_literal' || pt === 'anonymous_initializer' ||
pt === 'control_structure_body' || pt === 'getter' || pt === 'setter'
) { scope = 'local'; break; }
if (pt === 'companion_object' || pt === 'object_declaration') { scope = 'const'; break; }
if (pt === 'class_declaration') { scope = 'instance'; break; }
}
if (scope === 'local') return null;
const binding = node.namedChildren.find((c) => c.type === 'binding_pattern_kind');
const isVal = binding != null && getNodeText(binding, source) === 'val';
return scope === 'instance' ? 'field' : isVal ? 'constant' : 'variable';
}
/**
* Accessors written on their OWN line parse as SIBLINGS of the property, not as
* children of it (same-line ones nest — see kotlinPropertyInitializers). Walking
* back over any accessors between us and the declaration finds the property an
* accessor belongs to; null when this accessor stands alone (a grammar
* accident, or an accessor on a destructured/local declaration).
*/
function kotlinAccessorOwner(node: SyntaxNode): SyntaxNode | null {
for (let p = node.previousNamedSibling; p; p = p.previousNamedSibling) {
if (p.type === 'getter' || p.type === 'setter') continue;
return p.type === 'property_declaration' ? p : null;
}
return null;
}
/** The sibling accessors that follow a property declaration, in source order. */
function kotlinFollowingAccessors(node: SyntaxNode): SyntaxNode[] {
const out: SyntaxNode[] = [];
for (let n = node.nextNamedSibling; n; n = n.nextNamedSibling) {
if (n.type !== 'getter' && n.type !== 'setter') break;
out.push(n);
}
return out;
}
/** Check if a node matches the `fun interface` misparse pattern */
function isFunInterfaceNode(node: SyntaxNode): boolean {
let hasFun = false;
@@ -88,48 +181,70 @@ export const kotlinExtractor: LanguageExtractor = {
// Kotlin properties (`val` / `var` / `const val`). The name nests as
// property_declaration → variable_declaration → simple_identifier, which the
// generic variable/field path can't read — so nothing was extracted before.
// Kind by enclosing scope: a singleton `object` / `companion object` (and a
// top-level property) holds *shared* values — `val`→`constant`,
// `var`→`variable` (the Scala-object rule; a `const val` is a `val`). A
// `class`/`interface`/`enum` instance `val`/`var` is per-instance state →
// `field` (never a value-ref target, like a Java instance `final`). A
// property inside a function body / `init` block / lambda is a local and is
// skipped entirely.
// Kind comes from kotlinPropertyKind.
if (node.type === 'property_declaration') {
const varDecl = node.namedChildren.find((c) => c.type === 'variable_declaration');
const nameNode = varDecl?.namedChildren.find((c) => c.type === 'simple_identifier');
if (!nameNode) return false; // destructuring `val (a,b)` etc. — leave to default
// Destructuring (`val (a, b) = makePair()`): no symbol is minted for the
// destructured names either way — declining just routes the node to
// extractField/extractVariable, which both find nothing for Kotlin and
// end in the same fn-ref scan. But the RHS is CODE, and it was vanishing
// whole. Consume the node here and walk it at the ENCLOSING scope (there
// is no symbol of its own to attribute it to).
if (!nameNode) {
for (const init of kotlinPropertyInitializers(node)) ctx.visitFunctionBody(init, '');
return true;
}
const name = getNodeText(nameNode, ctx.source);
if (!name) return false;
// Walk to the nearest enclosing definition: a function body / init / lambda
// means it's a local; `object`/`companion object` is a constant scope; a
// `class_declaration` (covers class/interface/enum) is an instance scope.
let scope: 'local' | 'const' | 'instance' = 'const';
for (let p = node.parent; p; p = p.parent) {
const pt = p.type;
if (
pt === 'function_body' || pt === 'function_declaration' ||
pt === 'lambda_literal' || pt === 'anonymous_initializer' ||
pt === 'control_structure_body' || pt === 'getter' || pt === 'setter'
) { scope = 'local'; break; }
if (pt === 'companion_object' || pt === 'object_declaration') { scope = 'const'; break; }
if (pt === 'class_declaration') { scope = 'instance'; break; }
const kind = kotlinPropertyKind(node, ctx.source);
if (kind == null) {
// A local — no node is minted, but the initializer is still code. Walk
// it at the ENCLOSING scope: an `init { }` block's `val q = load()` is
// the CLASS calling load, and it used to disappear entirely (only the
// block's bare statements survived).
for (const init of kotlinPropertyInitializers(node)) ctx.visitFunctionBody(init, '');
return true;
}
if (scope === 'local') return true; // a local — don't extract
const binding = node.namedChildren.find((c) => c.type === 'binding_pattern_kind');
const isVal = binding != null && getNodeText(binding, ctx.source) === 'val';
const kind = scope === 'instance' ? 'field' : isVal ? 'constant' : 'variable';
const typeNode = node.childForFieldName('type');
const sig = typeNode
? `${isVal ? 'val' : 'var'} ${name}: ${getNodeText(typeNode, ctx.source)}`
: undefined;
ctx.createNode(kind, name, node, { signature: sig });
const created = ctx.createNode(kind, name, node, { signature: sig });
// Walk the initializer ATTRIBUTED to the declared symbol (#693, the Go
// fix, ported to Kotlin): the hook consumes this subtree, so without an
// explicit walk a lambda / SAM / object initializer
// (`private val cb = Runnable { target() }` — the idiomatic Android
// callback field) contributed NO call edge at all, and everything reached
// only through such a callback looked like it had no callers.
// The property also OWNS any accessor written on its own line, which the
// grammar makes a following SIBLING rather than a child; those bodies used
// to attribute to the enclosing class. Consumed here so the accessor
// branch below can skip them without any cross-node state.
const inits = created
? [...kotlinPropertyInitializers(node), ...kotlinFollowingAccessors(node)]
: [];
if (created && inits.length > 0) {
ctx.pushScope(created.id);
for (const init of inits) ctx.visitFunctionBody(init, created.id);
ctx.popScope();
}
return true;
}
// An own-line accessor already walked by its owning property above. The
// ownership test re-derives the property's kind rather than remembering it:
// a destructured or local declaration mints no node, so its accessors were
// NOT consumed and must keep falling through to the normal recursion.
if (node.type === 'getter' || node.type === 'setter') {
const owner = kotlinAccessorOwner(node);
return owner != null && kotlinPropertyKind(owner, ctx.source) != null;
}
// Handle Kotlin `fun interface` declarations.
// Tree-sitter-kotlin doesn't support `fun interface` syntax (Kotlin 1.4+).
// It produces two different misparse patterns:
+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;
}