From 67cc4c6d94a5229eb3fd4b40c40ba88e88832bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Fri, 22 May 2026 10:40:32 +0100 Subject: [PATCH 1/3] fix(lang-kotlin): cross-file iterable return propagation (#1759) (#1775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs surfaced in REGISTRY_PRIMARY_KOTLIN=1 forced mode: 1. `import models.getRepo` silently resolved to `models/User.kt` (the first `.kt` file inside `models/` by iteration order) when no file was named after the symbol. `findKotlinFile` returned a single directory child as a fallback, so the importer's module-scope mirror only ever picked up the first arbitrary candidate — `getUser → User` landed but `getRepo → Repo` never did, and downstream `repo.save()` resolution fell through to no edge. 2. `for (x in importedCallable())` produced no for-loop type binding when the callee's return type lived in another file, because `inferKotlinIterableElementType`'s call-expression arm consulted only the local file's `returnTypes` map. Fix: - Split `findKotlinFile` into `findKotlinExactOrSuffix` (exact / suffix match only) and `findKotlinDirectoryChild` (legacy single-child fallback). Add `findKotlinPackageFiles` returning every `.kt`/`.kts` file inside a package directory. The resolver now fans out the stripped path through `findKotlinExactOrSuffix → findKotlinPackageFiles`, returning a `readonly string[]` candidate set. The finalize pass walks each candidate and picks the one whose `localDefs` actually export the imported name — exactly the multi-target contract `FinalizeHooks.resolveImportTarget` already supports. - `inferKotlinIterableElementType` for `call_expression` now falls back to the callee's identifier text when the local return-type map has no entry. `propagateImportedReturnTypes` chain-follows `loopvar → callee → ElementType` once the imported `callee → Element` mirror lands at module scope (which now works thanks to fix #1). Verification (REGISTRY_PRIMARY_KOTLIN=1): - Forced-mode: 21 -> 18 failing of 175 (3 fewer; tests 487, 1242, 1251 in test/integration/resolvers/kotlin.test.ts now green). - Default-mode Kotlin: 175/175 unchanged. - Full resolver suite: 2216/2216 unchanged (incl. `kotlin-calls` `util.OneArg.writeAudit` regression check at line 176). - Remaining 18 failures are tracked by sibling sub-issues (#1758, #1760, #1761, #1762, #1763). Does NOT add Kotlin to MIGRATED_LANGUAGES per parent #1746 flip criteria. Closes #1759. Refs #1746. Co-authored-by: Test --- .../ingestion/languages/kotlin/captures.ts | 11 +- .../languages/kotlin/import-target.ts | 114 +++++++++++++++--- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts index ca8d34da3..1c5f71a27 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts @@ -411,7 +411,16 @@ function inferKotlinIterableElementType( const callee = iterable.namedChildren.find((child) => child.type === 'simple_identifier'); if (callee === undefined) return null; const raw = returnTypes.get(callee.text); - return raw === undefined ? null : kotlinContainerElementType(raw, 'values'); + if (raw !== undefined) return kotlinContainerElementType(raw, 'values'); + // Cross-file fallback (#1759): the callee's return type is unknown + // locally because the function lives in another file. Emit the + // callee name itself as the binding's rawName; `propagateImported + // ReturnTypes` will chain-follow `loopvar → callee → ` + // once the imported module's `callee → ElementType` mirror lands at + // module scope. If `callee` isn't actually an imported callable + // (e.g. a local lambda or unrelated symbol), chain-follow fails + // safely and no edge is emitted. + return callee.text; } return null; diff --git a/gitnexus/src/core/ingestion/languages/kotlin/import-target.ts b/gitnexus/src/core/ingestion/languages/kotlin/import-target.ts index 63b89b113..71d6a6b53 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin/import-target.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin/import-target.ts @@ -8,7 +8,7 @@ export interface KotlinResolveContext { export function resolveKotlinImportTarget( parsedImport: ParsedImport, workspaceIndex: WorkspaceIndex, -): string | null { +): string | readonly string[] | null { const ctx = workspaceIndex as KotlinResolveContext | undefined; if ( ctx === undefined || @@ -25,22 +25,49 @@ export function resolveKotlinImportTarget( : parsedImport.targetRaw; const pathLike = target.replace(/\./g, '/'); + // Resolution tiers, most-specific first: + // 1. The full `pathLike` matches a `.kt`/`.kts` file directly + // (`import util.User` → `util/User.kt`). + // 2. Stripped (last-segment removed) `pathLike` matches a file + // directly (`import util.OneArg.writeAudit` → `util/OneArg.kt`, + // a class-or-object holding `writeAudit`). + // 3. Stripped `pathLike` matches a *package directory* — fan out to + // every `.kt`/`.kts` file inside it (`import models.getRepo` → + // `[models/User.kt, models/Repo.kt]`). The finalize pass walks + // each candidate and picks the one whose `localDefs` actually + // export the imported name (#1759). + // 4. Progressive prefix strip for deeper namespace aliases that + // don't map 1:1 to directories. + const stripped = pathLike.split('/').slice(0, -1).join('/'); return ( findKotlinFile(ctx.allFilePaths, pathLike) ?? - findKotlinFile(ctx.allFilePaths, pathLike.split('/').slice(0, -1).join('/')) ?? + findKotlinExactOrSuffix(ctx.allFilePaths, stripped) ?? + findKotlinPackageFiles(ctx.allFilePaths, stripped) ?? findByProgressivePrefixStrip(ctx.allFilePaths, pathLike) ); } function findKotlinFile(allFilePaths: ReadonlySet, pathLike: string): string | null { + return ( + findKotlinExactOrSuffix(allFilePaths, pathLike) ?? + findKotlinDirectoryChild(allFilePaths, pathLike) + ); +} + +/** Exact (`file === pathLike+ext`) or suffix (`file ends with /pathLike+ext`) + * match — does NOT fall back to picking an arbitrary file inside a + * `pathLike/` directory. Used by the stripped-path tier in + * `resolveKotlinImportTarget` so a package import like `models.getRepo` + * delegates to `findKotlinPackageFiles` (multi-file fan-out) instead of + * silently committing to the first directory child. */ +function findKotlinExactOrSuffix( + allFilePaths: ReadonlySet, + pathLike: string, +): string | null { if (pathLike === '') return null; const extensions = ['.kt', '.kts']; const suffix = `/${pathLike}`; - const dirPrefix = `${pathLike}/`; - const suffixDirPrefix = `/${dirPrefix}`; - let suffixFile: string | null = null; - let directoryChild: string | null = null; for (const raw of allFilePaths) { const file = raw.replace(/\\/g, '/'); @@ -49,18 +76,73 @@ function findKotlinFile(allFilePaths: ReadonlySet, pathLike: string): st if (file === `${pathLike}${ext}`) return raw; if (suffixFile === null && file.endsWith(`${suffix}${ext}`)) suffixFile = raw; } - if (directoryChild === null) { - const atRoot = file.startsWith(dirPrefix); - const atNested = file.includes(suffixDirPrefix); - if (atRoot || atNested) { - const idx = atRoot ? 0 : file.indexOf(suffixDirPrefix) + 1; - const after = file.slice(idx + dirPrefix.length); - if (after.length > 0 && !after.includes('/')) directoryChild = raw; - } - } } - return suffixFile ?? directoryChild; + return suffixFile; +} + +/** First directory child of `pathLike/` — preserves the legacy single- + * file fallback for cases where `pathLike` itself is an unqualified + * package reference (rare in real Kotlin code; some fixtures rely on + * it). Multi-file package fan-out goes through + * `findKotlinPackageFiles` instead. */ +function findKotlinDirectoryChild( + allFilePaths: ReadonlySet, + pathLike: string, +): string | null { + if (pathLike === '') return null; + const extensions = ['.kt', '.kts']; + const dirPrefix = `${pathLike}/`; + const suffixDirPrefix = `/${dirPrefix}`; + + for (const raw of allFilePaths) { + const file = raw.replace(/\\/g, '/'); + if (!extensions.some((ext) => file.endsWith(ext))) continue; + const atRoot = file.startsWith(dirPrefix); + const atNested = file.includes(suffixDirPrefix); + if (!atRoot && !atNested) continue; + const idx = atRoot ? 0 : file.indexOf(suffixDirPrefix) + 1; + const after = file.slice(idx + dirPrefix.length); + if (after.length > 0 && !after.includes('/')) return raw; + } + + return null; +} + +/** + * Return every `.kt`/`.kts` file inside the package directory `dirPath` + * (e.g. `models` → `['models/User.kt', 'models/Repo.kt']`). Used as a + * fallback when an import like `models.getRepo` does not resolve to a + * file named after the symbol — in Kotlin the symbol can live in any + * file inside the package directory. The finalize pass walks each + * candidate and picks the one whose `localDefs` actually export the + * imported name (#1759). + */ +function findKotlinPackageFiles( + allFilePaths: ReadonlySet, + dirPath: string, +): readonly string[] | null { + if (dirPath === '') return null; + const extensions = ['.kt', '.kts']; + const dirPrefix = `${dirPath}/`; + const suffixDirPrefix = `/${dirPrefix}`; + const out: string[] = []; + + for (const raw of allFilePaths) { + const file = raw.replace(/\\/g, '/'); + if (!extensions.some((ext) => file.endsWith(ext))) continue; + const atRoot = file.startsWith(dirPrefix); + const atNested = file.includes(suffixDirPrefix); + if (!atRoot && !atNested) continue; + const idx = atRoot ? 0 : file.indexOf(suffixDirPrefix) + 1; + const after = file.slice(idx + dirPrefix.length); + // Direct children only — `models/sub/Util.kt` is a different package + // (`models.sub`) and must not be merged with `models`. + if (after.length === 0 || after.includes('/')) continue; + out.push(raw); + } + + return out.length === 0 ? null : out; } function findByProgressivePrefixStrip( From 64efc202f68678522ae2ec3f613983d0197472fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Fri, 22 May 2026 10:40:46 +0100 Subject: [PATCH 2/3] fix(lang-kotlin): method-chain fixpoint receiver types (#1760) (#1776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1760. Multi-step intra-file chains like val user = getUser() val addr = user.address val city = addr.getCity() city.save() produced no `CALLS` edge for `city.save()` because the Kotlin extractor only inferred property types for `simple_identifier` values (`val x = y`) and call expressions with simple-identifier callees (`val x = fn()`). Navigation expressions (`val addr = user.address`) and call expressions with navigation-expression callees (`val city = addr.getCity()`) returned null, leaving `addr` and `city` unbound — the chain broke two hops before `city.save()`. Implementation: - `collectKotlinClassMembers(rootNode)` indexes per-file class fields (primary-constructor `val`/`var` params + body property declarations) and method return types. Per-file scope matches the existing extractor design. - `inferKotlinPropertyType` gains two new cases: 1. `navigation_expression` value — receiver type via `localTypes`, field type via `classMembers.fields`. 2. `call_expression` with `navigation_expression` callee — receiver type via `localTypes`, method return type via `classMembers.methods`. Both return null when any link is unknown (safe / over-conservative). Verification (REGISTRY_PRIMARY_KOTLIN=1): - Forced-mode: 21 -> 20 failing of 175 (1 fewer; test 1491 in `test/integration/resolvers/kotlin.test.ts` now green). - Default-mode Kotlin: 175/175 unchanged. - Full resolver suite: 2216/2216 unchanged. - Remaining 20 failures are tracked by sibling sub-issues (#1758, #1759, #1761, #1762, #1763). Does NOT add Kotlin to MIGRATED_LANGUAGES per parent #1746 flip criteria. Closes #1760. Refs #1746. Co-authored-by: Test --- .../ingestion/languages/kotlin/captures.ts | 141 +++++++++++++++++- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts index 1c5f71a27..a0f376da7 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts @@ -290,10 +290,11 @@ function synthesizeKotlinLocalAssignmentBindings( returnTypes: ReadonlyMap, ): CaptureMatch[] { const out: CaptureMatch[] = []; + const classMembers = collectKotlinClassMembers(rootNode); for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) { const localTypes = new Map(); for (const prop of descendantsOfType(fnNode, 'property_declaration')) { - const inferred = inferKotlinPropertyType(prop, localTypes, returnTypes); + const inferred = inferKotlinPropertyType(prop, localTypes, returnTypes, classMembers); if (inferred === null) continue; localTypes.set(inferred.name.text, inferred.rawType); if (inferred.synthetic) { @@ -316,6 +317,77 @@ function synthesizeKotlinLocalAssignmentBindings( return out; } +interface KotlinClassMembers { + /** className → fieldName → raw type text */ + readonly fields: ReadonlyMap>; + /** className → methodName → raw return type text */ + readonly methods: ReadonlyMap>; +} + +/** + * Per-file class-member index — primary-constructor `val`/`var` params, + * body property declarations, and method return types. Used by + * `inferKotlinPropertyType` to walk single-level field and method chains + * like `val addr = user.address` and `val city = addr.getCity()` (#1760). + * + * Indexes by simple class name only. Multi-class collisions inside a + * single file will pick whichever class was visited last for that name + * — acceptable because Kotlin forbids same-name top-level classes in + * one file and per-file resolution is the design boundary here. + */ +function collectKotlinClassMembers(rootNode: SyntaxNode): KotlinClassMembers { + const fields = new Map>(); + const methods = new Map>(); + for (const cls of descendantsOfType(rootNode, 'class_declaration')) { + const className = cls.namedChildren.find((child) => child.type === 'type_identifier')?.text; + if (className === undefined) continue; + const fmap = fields.get(className) ?? new Map(); + const mmap = methods.get(className) ?? new Map(); + + const primary = cls.namedChildren.find((child) => child.type === 'primary_constructor'); + if (primary !== undefined) { + for (const param of primary.namedChildren) { + if (param.type !== 'class_parameter') continue; + // Constructor params are class fields ONLY when prefixed with + // `val`/`var` (binding_pattern_kind). Plain `fn(x: Int)`-style + // params remain locals to the constructor. + if (param.namedChildren.find((c) => c.type === 'binding_pattern_kind') === undefined) { + continue; + } + const fname = param.namedChildren.find((c) => c.type === 'simple_identifier')?.text; + const ftype = param.namedChildren.find((c) => isKotlinTypeNode(c))?.text; + if (fname !== undefined && ftype !== undefined) fmap.set(fname, ftype); + } + } + + const body = cls.namedChildren.find((child) => child.type === 'class_body'); + if (body !== undefined) { + for (const member of body.namedChildren) { + if (member.type === 'property_declaration') { + const v = member.namedChildren.find((c) => c.type === 'variable_declaration'); + const fname = v?.namedChildren.find((c) => c.type === 'simple_identifier')?.text; + const ftype = v?.namedChildren.find((c) => isKotlinTypeNode(c))?.text; + if (fname !== undefined && ftype !== undefined) fmap.set(fname, ftype); + } else if (member.type === 'function_declaration') { + const mname = member.namedChildren.find((c) => c.type === 'simple_identifier')?.text; + const paramsIdx = member.namedChildren.findIndex( + (c) => c.type === 'function_value_parameters', + ); + const rtype = + paramsIdx < 0 + ? undefined + : member.namedChildren.slice(paramsIdx + 1).find((c) => isKotlinTypeNode(c))?.text; + if (mname !== undefined && rtype !== undefined) mmap.set(mname, rtype); + } + } + } + + fields.set(className, fmap); + methods.set(className, mmap); + } + return { fields, methods }; +} + function collectKotlinLocalTypeTexts( fnNode: SyntaxNode, returnTypes: ReadonlyMap, @@ -357,6 +429,7 @@ function inferKotlinPropertyType( prop: SyntaxNode, localTypes: ReadonlyMap, returnTypes: ReadonlyMap, + classMembers?: KotlinClassMembers, ): { name: SyntaxNode; rawType: string; source: SyntaxNode; synthetic: boolean } | null { const variable = prop.namedChildren.find((child) => child.type === 'variable_declaration'); const name = variable?.namedChildren.find((child) => child.type === 'simple_identifier'); @@ -375,18 +448,74 @@ function inferKotlinPropertyType( return rawType === undefined ? null : { name, rawType, source: value, synthetic: true }; } + if (value?.type === 'navigation_expression') { + // `val addr = user.address` — receiver type → field on that class (#1760). + const chained = inferKotlinNavigationFieldType(value, localTypes, classMembers); + if (chained === null) return null; + return { name, rawType: chained, source: value, synthetic: true }; + } + if (value?.type === 'call_expression') { - const callee = value.namedChildren.find((child) => child.type === 'simple_identifier'); + const callee = value.namedChildren.find( + (child) => child.type === 'simple_identifier' || child.type === 'navigation_expression', + ); if (callee === undefined) return null; - const rawType = - returnTypes.get(callee.text) ?? (isUppercaseName(callee.text) ? callee.text : null); - if (rawType === null) return null; - return { name, rawType, source: callee, synthetic: true }; + if (callee.type === 'simple_identifier') { + const rawType = + returnTypes.get(callee.text) ?? (isUppercaseName(callee.text) ? callee.text : null); + if (rawType === null) return null; + return { name, rawType, source: callee, synthetic: true }; + } + // `val city = addr.getCity()` — receiver type → method return on that class (#1760). + const chained = inferKotlinNavigationCallReturnType(callee, localTypes, classMembers); + if (chained === null) return null; + return { name, rawType: chained, source: callee, synthetic: true }; } return null; } +/** Resolve `receiver.field` → field's declared type, where `receiver` + * is a simple identifier whose type is in `localTypes` and `field` + * is declared on that type in `classMembers.fields`. Returns null + * when any link in the chain is unknown — safe over-conservative. */ +function inferKotlinNavigationFieldType( + nav: SyntaxNode, + localTypes: ReadonlyMap, + classMembers: KotlinClassMembers | undefined, +): string | null { + if (classMembers === undefined) return null; + const receiver = nav.namedChild(0); + if (receiver === null || receiver.type !== 'simple_identifier') return null; + const member = nav.namedChildren + .find((c) => c.type === 'navigation_suffix') + ?.namedChildren.find((c) => c.type === 'simple_identifier')?.text; + if (member === undefined) return null; + const recvType = localTypes.get(receiver.text); + if (recvType === undefined) return null; + return classMembers.fields.get(normalizeKotlinType(recvType))?.get(member) ?? null; +} + +/** Resolve `receiver.method()` → method's declared return type, where + * `receiver` is a simple identifier whose type is in `localTypes` and + * `method` is declared on that type in `classMembers.methods`. */ +function inferKotlinNavigationCallReturnType( + navCallee: SyntaxNode, + localTypes: ReadonlyMap, + classMembers: KotlinClassMembers | undefined, +): string | null { + if (classMembers === undefined) return null; + const receiver = navCallee.namedChild(0); + if (receiver === null || receiver.type !== 'simple_identifier') return null; + const methodName = navCallee.namedChildren + .find((c) => c.type === 'navigation_suffix') + ?.namedChildren.find((c) => c.type === 'simple_identifier')?.text; + if (methodName === undefined) return null; + const recvType = localTypes.get(receiver.text); + if (recvType === undefined) return null; + return classMembers.methods.get(normalizeKotlinType(recvType))?.get(methodName) ?? null; +} + function inferKotlinIterableElementType( iterable: SyntaxNode, localTypes: ReadonlyMap, From f72d9a99c6250a669078bf2e9daf580be22a821d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Fri, 22 May 2026 10:40:59 +0100 Subject: [PATCH 3/3] fix(lang-kotlin): virtual dispatch via constructor type override (#1762) (#1778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1762. `val animal: Animal = Dog(); animal.speak()` resolved to `Animal.speak` (or no edge to Dog at all) under `REGISTRY_PRIMARY_KOTLIN=1` because the Kotlin scope query emits BOTH an annotation type-binding (`animal -> Animal`) and a constructor- inferred type-binding (`animal -> Dog`). The generic scope-extractor ranks annotation sources higher than constructor-inferred sources (see `typeBindingStrength` in scope-extractor.ts), so the annotation always won and `animal.speak()` dispatched against the static type. Kotlin's virtual dispatch semantics expect the dynamic type — the overriding `Dog.speak` should win when the RHS is a constructor call, because that's what runs at runtime. Fix: in `emitKotlinScopeCaptures`, suppress the `@type-binding. annotation` capture when the underlying `property_declaration` has a `call_expression` value sibling. The constructor-inferred capture remains, becomes the sole binding for the variable, and receiver-bound resolution dispatches against the constructed class (and walks its MRO). This is intentionally Kotlin-specific — flipping precedence globally would change behavior for other languages whose static-type annotations are still the right binding when present. Kotlin is the language where the constructor RHS is the dispatch target by design. Verification (REGISTRY_PRIMARY_KOTLIN=1): - Forced-mode: 21 -> 20 failing of 175 (1 fewer; test 1715 in `test/integration/resolvers/kotlin.test.ts` now green). - Default-mode Kotlin: 175/175 unchanged. - Full resolver suite: 2216/2216 unchanged. - Remaining 20 failures are tracked by sibling sub-issues (#1758, #1759, #1760, #1761, #1763). Does NOT add Kotlin to MIGRATED_LANGUAGES per parent #1746 flip criteria. Closes #1762. Refs #1746. Co-authored-by: Test --- .../ingestion/languages/kotlin/captures.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts index a0f376da7..4864d7d38 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts @@ -1,4 +1,4 @@ -import type { Capture, CaptureMatch } from 'gitnexus-shared'; +import type { Capture, CaptureMatch, Range } from 'gitnexus-shared'; import { findNodeAtRange, nodeToCapture, @@ -73,6 +73,30 @@ export function emitKotlinScopeCaptures( if (navNode === null || !shouldEmitReadMember(navNode)) continue; } + // Virtual dispatch via constructor type (#1762). When a property + // declaration carries BOTH an explicit type annotation AND a + // constructor-style call value (e.g. `val animal: Animal = Dog()`), + // suppress the annotation capture so the constructor-inferred + // binding wins. This matches Kotlin's virtual dispatch semantics: + // `animal.speak()` should resolve to the overriding `Dog.speak` + // (the dynamic type), not `Animal.speak` (the static annotation). + // + // The annotation source has higher precedence than constructor- + // inferred in the generic scope-extractor (see + // `typeBindingStrength` in scope-extractor.ts), so the only way to + // make the constructor type prevail is to drop the annotation at + // emission time. + if ( + grouped['@type-binding.annotation'] !== undefined && + grouped['@type-binding.name'] !== undefined && + grouped['@type-binding.type'] !== undefined + ) { + const annotation = grouped['@type-binding.annotation']!; + if (propertyDeclHasConstructorValue(tree.rootNode, annotation.range)) { + continue; + } + } + if (grouped['@scope.function'] !== undefined) { out.push(grouped); const fnNode = findNodeAtRange( @@ -646,6 +670,21 @@ function shouldEmitReadMember(navNode: SyntaxNode): boolean { return true; } +/** True when the property_declaration anchored at `range` has a + * `call_expression` value sibling (i.e. `val x: T = Foo()`). Used to + * suppress the explicit-annotation type-binding capture so the + * constructor-inferred binding wins (#1762). */ +function propertyDeclHasConstructorValue(rootNode: SyntaxNode, range: Range): boolean { + const propNode = findNodeAtRange(rootNode, range, 'property_declaration'); + if (propNode === null) return false; + const variable = propNode.namedChildren.find((c) => c.type === 'variable_declaration'); + if (variable === undefined) return false; + const value = propNode.namedChildren.find( + (c) => c.id !== variable.id && c.type !== 'binding_pattern_kind', + ); + return value?.type === 'call_expression'; +} + function callArguments(callNode: SyntaxNode): SyntaxNode[] { const suffix = callNode.namedChildren.find((child) => child.type === 'call_suffix'); if (suffix === undefined) return [];