Kotlin: map suspend { } to a method invocation at every language level - #8557
Merged
Conversation
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.
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.
greg-at-moderne
approved these changes
Aug 20, 2026
Contributor
|
TBH, I didn't the PR description, only the code. |
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's changed?
Relates to #8542, which raises the Kotlin compiler to 2.4.10 and adds the
KOTLIN_2_3andKOTLIN_2_4language levels. This PR addsKOTLIN_2_3on 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 staysKOTLIN_2_2. Expect a trivial textual conflict inKotlinParser'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 tokotlin.suspend, so theKtCallExpressionhas 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 enclosingFirProperty,getCallTypethrowsUnsupportedOperationException: Unsupported call type: …FirPropertyImpl, andvisitKtFilereplaces the entire enclosing top-level declaration with aJ.Unknown.At language level 2.3/2.4 the whole
fun method()became aJ.Unknowncarrying aParseExceptionResultmarker. Nothing surfaced it: zeroParseErrors, and no output even withlogCompilationWarningsAndErrors(true).Three narrow guards in
PsiElementAssociations, all keyed on "the FIR found is not about this PSI":getCallTypereturnsnull— whichvisitCallExpressionalready maps to aJ.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 onFirResolvedArgumentList.type()no longer attributes aFirVariableto aKtSimpleNameExpressionof a different name; without this the calleesuspendpicked up the enclosinglambdavariable as itsfieldType.methodInvocationType()resolveskotlin.suspendfrom the session's symbol provider as a last resort, so the invocation keeps a non-nullJavaType.Methodat 2.3+ instead of trading aJ.Unknownfor an untyped invocation.Two related improvements:
J.Unknownfallback 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 (deepPrefixisendFixPrefixAndInfix, purely PSI-derived), so the fallback uses the prefix a successful statement would have.rewrite-kotlin'sAssertions.validateTypesnever implemented theTypeValidation.unknown()check thatrewrite-java's has, which is why the degradation was invisible in green tests. It now fails with theParseExceptionResultmessage and the offending source.Anything in particular you'd like reviewers to focus on?
resolvedAwayCallTypematchingkotlin.suspendby 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-declaredsuspendfunction resolves normally and never reaches it. The alternative is aJ.MethodInvocationwith a null method type, which is a type-attribution regression at 2.3+.TypeValidation.unknown()inrewrite-kotlinmay surface latentJ.Unknowns in downstream Kotlin test suites. Tests can opt out withtypeValidationOptions.Checklist
Testing notes
LambdaTest.suspendLambdaAtEveryLanguageLevelis 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. Onmainit fails at[4] languageLevel = KOTLIN_2_3without 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:testsuite is green there. Onmain,:rewrite-kotlin:test,:rewrite-gradle:testand:rewrite-android:testare green.The narrowed blast radius has no committable regression test — instrumenting the
visitKtFilecatch across the whole rewrite-kotlin corpus at 2.2 shows zero failing declarations today. It was validated on the 2.4 branch with thegetCallTypefix reverted: only theval lambda …statement becomesJ.Unknown, the sibling statements and the other members keep their types, and the file still prints byte-identically.