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 <test@example.com>
This commit is contained in:
@@ -290,10 +290,11 @@ function synthesizeKotlinLocalAssignmentBindings(
|
||||
returnTypes: ReadonlyMap<string, string>,
|
||||
): CaptureMatch[] {
|
||||
const out: CaptureMatch[] = [];
|
||||
const classMembers = collectKotlinClassMembers(rootNode);
|
||||
for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
|
||||
const localTypes = new Map<string, string>();
|
||||
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<string, ReadonlyMap<string, string>>;
|
||||
/** className → methodName → raw return type text */
|
||||
readonly methods: ReadonlyMap<string, ReadonlyMap<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, Map<string, string>>();
|
||||
const methods = new Map<string, Map<string, string>>();
|
||||
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<string, string>();
|
||||
const mmap = methods.get(className) ?? new Map<string, string>();
|
||||
|
||||
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<string, string>,
|
||||
@@ -357,6 +429,7 @@ function inferKotlinPropertyType(
|
||||
prop: SyntaxNode,
|
||||
localTypes: ReadonlyMap<string, string>,
|
||||
returnTypes: ReadonlyMap<string, string>,
|
||||
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<string, string>,
|
||||
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<string, string>,
|
||||
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<string, string>,
|
||||
|
||||
Reference in New Issue
Block a user