Skip to content

Kotlin: map suspend { } to a method invocation at every language level - #8557

Merged
timtebeek merged 2 commits into
mainfrom
tim/kotlin-suspend-lambda-firproperty
Aug 20, 2026
Merged

Kotlin: map suspend { } to a method invocation at every language level#8557
timtebeek merged 2 commits into
mainfrom
tim/kotlin-suspend-lambda-firproperty

Conversation

@timtebeek

@timtebeek timtebeek commented Aug 19, 2026

Copy link
Copy Markdown
Member

What's changed?

Relates to #8542, which raises the Kotlin compiler to 2.4.10 and adds the KOTLIN_2_3 and KOTLIN_2_4 language levels. This PR adds KOTLIN_2_3 on its own, because the pinned compiler is already 2.3.20 and without that level the regression test cannot reach the resolution change it guards; the builder default stays KOTLIN_2_2. Expect a trivial textual conflict in KotlinParser's enum and switch statements with #8542, whichever lands second.

Since Kotlin 2.3 the compiler resolves suspend { } to a suspending lambda without emitting a call to kotlin.suspend, so the KtCallExpression has no FIR of its own. PsiElementAssociations.fir() walks up the PSI tree when a node is absent from its element map, so it returns the enclosing FirProperty, getCallType throws UnsupportedOperationException: Unsupported call type: …FirPropertyImpl, and visitKtFile replaces the entire enclosing top-level declaration with a J.Unknown.

fun method ( ) {
    val lambda : suspend ( ) -> Int = suspend { 1 }
}

At language level 2.3/2.4 the whole fun method() became a J.Unknown carrying a ParseExceptionResult marker. Nothing surfaced it: zero ParseErrors, and no output even with logCompilationWarningsAndErrors(true).

Three narrow guards in PsiElementAssociations, all keyed on "the FIR found is not about this PSI":

  • getCallType returns null — which visitCallExpression already maps to a J.MethodInvocation, the same shape 2.2 produces — when the FIR's source PSI is not the expression asked about. It still throws when the FIR really does belong to the expression. This also covers argument position (take(suspend { 1 })), where the walk-up lands on FirResolvedArgumentList.
  • type() no longer attributes a FirVariable to a KtSimpleNameExpression of a different name; without this the callee suspend picked up the enclosing lambda variable as its fieldType.
  • methodInvocationType() resolves kotlin.suspend from the session's symbol provider as a last resort, so the invocation keeps a non-null JavaType.Method at 2.3+ instead of trading a J.Unknown for an untyped invocation.

Two related improvements:

  • Blast radius. The J.Unknown fallback now also wraps each class-body member and each block statement, so a mapping failure costs one statement instead of an entire top-level declaration. Prefix handling is unchanged (deepPrefix is endFixPrefixAndInfix, purely PSI-derived), so the fallback uses the prefix a successful statement would have.
  • Visibility. rewrite-kotlin's Assertions.validateTypes never implemented the TypeValidation.unknown() check that rewrite-java's has, which is why the degradation was invisible in green tests. It now fails with the ParseExceptionResult message and the offending source.

Anything in particular you'd like reviewers to focus on?

  • Whether resolvedAwayCallType matching kotlin.suspend by name is acceptable. It only runs for a call the compiler left no FIR for, with a single lambda argument and no value-argument list; a user-declared suspend function resolves normally and never reaches it. The alternative is a J.MethodInvocation with a null method type, which is a type-attribution regression at 2.3+.
  • Honoring TypeValidation.unknown() in rewrite-kotlin may surface latent J.Unknowns in downstream Kotlin test suites. Tests can opt out with typeValidationOptions.

Checklist

Testing notes

  • LambdaTest.suspendLambdaAtEveryLanguageLevel is parameterized with @EnumSource(names = "KOTLIN_2_\d+", mode = MATCH_ALL), so it covers 2.0–2.3 here and picks up 2.4 automatically once migrate to Kotlin 2.4.10 #8542 lands — no dependency on that branch. On main it fails at [4] languageLevel = KOTLIN_2_3 without the parser change and passes with it.

  • Also verified against the branch of migrate to Kotlin 2.4.10 #8542 (barbulescu/kotlin-2.4.10): the new test fails at 2.3 and 2.4 without this change and passes with it, and the full :rewrite-kotlin:test suite is green there. On main, :rewrite-kotlin:test, :rewrite-gradle:test and :rewrite-android:test are green.

The narrowed blast radius has no committable regression test — instrumenting the visitKtFile catch across the whole rewrite-kotlin corpus at 2.2 shows zero failing declarations today. It was validated on the 2.4 branch with the getCallType fix reverted: only the val lambda … statement becomes J.Unknown, the sibling statements and the other members keep their types, and the file still prints byte-identically.

Since Kotlin 2.3 the compiler resolves `suspend { }` to a suspending
lambda without emitting a call to `kotlin.suspend`, so the call
expression has no FIR of its own. `PsiElementAssociations.fir()` then
walks up the PSI tree and returns the enclosing declaration's FIR, which
made `getCallType` throw and turned the whole top-level declaration into
a `J.Unknown`.

Fall back to a method invocation when the FIR found does not belong to
the expression asked about, stop attributing an enclosing variable's type
to an identifier of a different name, and resolve `kotlin.suspend` from
the symbol provider so the invocation keeps its method type.

Also narrow the blast radius of a mapping failure from the enclosing
top-level declaration to the class member or block statement that failed,
and honor `TypeValidation.unknown()` in the Kotlin test assertions, which
previously let a `J.Unknown` pass unnoticed.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 19, 2026
@timtebeek timtebeek mentioned this pull request Aug 19, 2026
4 tasks
@timtebeek timtebeek added bug Something isn't working kotlin parser labels Aug 19, 2026
@timtebeek
timtebeek marked this pull request as ready for review August 19, 2026 21:49
The pinned compiler is already 2.3.20, but `KotlinLanguageLevel` stopped at
`KOTLIN_2_2`, capping `LanguageVersion` below the resolution change that
drops the `kotlin.suspend` call. Without 2.3 the parameterized test could
not reproduce the failure it guards. The builder default stays 2.2.

Also build the `ParseExceptionResult` from `KotlinParser.class` rather than
constructing a throwaway parser for every failed element.
@github-project-automation github-project-automation Bot moved this from In Progress to Ready to Review in OpenRewrite Aug 20, 2026
@greg-at-moderne

Copy link
Copy Markdown
Contributor

TBH, I didn't the PR description, only the code.

@timtebeek
timtebeek merged commit 6fa892e into main Aug 20, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Aug 20, 2026
@timtebeek
timtebeek deleted the tim/kotlin-suspend-lambda-firproperty branch August 20, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working kotlin parser

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants