Repository: openrewrite/rewrite (2026-04-17T10:10:45Z)

Issue:
Kotlin parser emits JavaType.Unknown as declaringType for methods on built-in primitive receivers (e.g. Char.toInt())
## What version of OpenRewrite are you using?

- rewrite 8.80.0-SNAPSHOT (rewrite-kotlin), regression since 8.79.4 / #7364.

## How are you running OpenRewrite?

Any recipe using `MethodMatcher` against a method call on a Kotlin built-in receiver.

## What is the smallest, simplest way to reproduce the problem?

Parse:

```kotlin
fun test(c: Char): Int {
    return c.toInt()
}
```

Inspect the `J.MethodInvocation` for `c.toInt()`:

- `method.getSelect().getType()` → `JavaType.Primitive` (`char`) ✓
- `method.getMethodType().getDeclaringType()` → `JavaType.Unknown` ✗

`MethodMatcher` patterns like `kotlin.Char toInt()` (or the JVM-normalized `char toInt()`) fail to match because the declaring type is `Unknown`, not the expected primitive/class.

Similar behavior was observed for methods on `kotlinx.serialization.modules.PolymorphicModuleBuilder<T>` and `SerializersModuleBuilder` in `kotlinx-serialization-core` (declaringType=`Unknown`, select carries the right type).

## What did you expect to see?

`methodType.getDeclaringType()` reflects the receiver type (e.g. `char` primitive or `kotlin.Char` FQN) so `MethodMatcher` patterns match reliably.

## What did you see instead?

`methodType.getDeclaringType() instanceof JavaType.Unknown`, causing every pattern-matching recipe targeting these methods to silently fail.

## Context

- This regressed after the JVM type-alignment change in #7364 (8.79.4). `rewrite-migrate-kotlin` works around it in `ReplaceKotlinMethod` by synthesizing the declaring type from the select's known type: https://github.com/moderneinc/rewrite-migrate-kotlin/commit/8f2ec1b — see `matches(MethodCall)` and the `JVM_PRIMITIVE_TO_KOTLIN_FQN` reverse map. Fixing this upstream would let recipes drop that fallback.

## Suggested fix

During type attribution in the Kotlin parser, resolve `declaringType` for built-in receivers (primitives, `kotlin.Any`, `kotlin.String`, `kotlin.Throwable`, etc.) instead of falling back to `Unknown`.


Discussion:

Patch:
diff --git a/rewrite-kotlin/src/main/kotlin/org/openrewrite/kotlin/KotlinTypeMapping.kt b/rewrite-kotlin/src/main/kotlin/org/openrewrite/kotlin/KotlinTypeMapping.kt
index b86cdd501a3..4c057c33a64 100644
--- a/rewrite-kotlin/src/main/kotlin/org/openrewrite/kotlin/KotlinTypeMapping.kt
+++ b/rewrite-kotlin/src/main/kotlin/org/openrewrite/kotlin/KotlinTypeMapping.kt
@@ -680,11 +680,18 @@ class KotlinTypeMapping(
             null
         )
         typeCache.put(signature, method)
-        var parentType = when {
+        // A method's declaring type is always a class even when its receiver is a Kotlin
+        // primitive (e.g. `kotlin.Char.toInt()` is declared on the `kotlin.Char` class, not
+        // the JVM `char` primitive). Bypass the primitive remap that `type()` applies to
+        // non-nullable Kotlin primitives by going through `asDeclaringType` / `classType`.
+        var parentType: JavaType? = when {
             function.symbol is FirConstructorSymbol -> type(function.returnTypeRef)
+            function.dispatchReceiverType is ConeClassLikeType ->
+                asDeclaringType(function.dispatchReceiverType as ConeClassLikeType)
             function.dispatchReceiverType != null -> type(function.dispatchReceiverType!!)
             function.symbol.getOwnerLookupTag()?.toRegularClassSymbol(firSession)?.fir != null -> {
-                type(function.symbol.getOwnerLookupTag()!!.toRegularClassSymbol(firSession)!!.fir)
+                val fir = function.symbol.getOwnerLookupTag()!!.toRegularClassSymbol(firSession)!!.fir
+                TypeUtils.asFullyQualified(classType(fir, firFile, signatureBuilder.signature(fir)))
             }
 
             parent is FirRegularClass || parent != null -> type(parent)


Test patch:
diff --git a/rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java b/rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java
index bdb957c9a0f..4a8994d495f 100644
--- a/rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java
+++ b/rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java
@@ -1816,6 +1816,36 @@ public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, Integer n) {
             );
         }
 
+        @Issue("https://github.com/openrewrite/rewrite/issues/7408")
+        @Test
+        void declaringTypeOnKotlinPrimitiveReceiver() {
+            rewriteRun(
+              kotlin(
+                """
+                  fun test(c: Char): Int {
+                      return c.toInt()
+                  }
+                  """,
+                spec -> spec.afterRecipe(cu -> {
+                    AtomicBoolean found = new KotlinIsoVisitor<AtomicBoolean>() {
+                        @Override
+                        public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, AtomicBoolean found) {
+                            if ("toInt".equals(method.getSimpleName())) {
+                                JavaType.FullyQualified declaringType = method.getMethodType().getDeclaringType();
+                                assertThat(declaringType).isNotInstanceOf(JavaType.Unknown.class);
+                                assertThat(declaringType.getFullyQualifiedName()).isEqualTo("kotlin.Char");
+                                assertThat(new MethodMatcher("kotlin.Char toInt()").matches(method)).isTrue();
+                                found.set(true);
+                            }
+                            return super.visitMethodInvocation(method, found);
+                        }
+                    }.reduce(cu, new AtomicBoolean());
+                    assertThat(found.get()).isTrue();
+                })
+              )
+            );
+        }
+
         @Issue("https://github.com/openrewrite/rewrite-kotlin/issues/590")
         @Test
         void callWithDefaultedGenericParameters() {