Skip to content

Kotlin recipe DSL: trailing-lambda parens + Java-static chain inner + bare-param after body#7709

Merged
jkschneider merged 2 commits into
mainfrom
kotlin-munich
May 17, 2026
Merged

Kotlin recipe DSL: trailing-lambda parens + Java-static chain inner + bare-param after body#7709
jkschneider merged 2 commits into
mainfrom
kotlin-munich

Conversation

@jkschneider
Copy link
Copy Markdown
Member

@jkschneider jkschneider commented May 17, 2026

Summary

  1. preserveTrailingLambdaShape was dropping parens around non-lambda args. Multi-arg calls with a trailing lambda (obj.method(arg) { lambda }) were rendering as obj.methodarg { lambda }. Caused by unconditionally adding OmitParentheses to the args container whenever the last arg carried TrailingLambdaArgument. Fix: gate OmitParentheses on padded.size() == 1. For multi-arg trailing-lambda calls, the marker on the lambda is sufficient — KotlinPrinter.visitArgumentsContainer already handles the rendering.

  2. Chain validator rejected Java-static inner segments. Patterns like Optional.of(x).get() were silently falling through to the imperative path (the pluginRequired runtime stub). Root cause: K2 wraps Java-platform-typed call results (Optional<T>!Optional<T>) in IrTypeOperatorCall(IMPLICIT_CAST), so the chain detector's rawReceiver is IrCall check was false. Fix: added unwrapImplicitCasts(expr) and called it at every IrCall-shape-check site in validateBeforeLambda.

  3. buildAfterTemplate missed bare-param after bodies. With (2) in place, Optional.of("hi").get() matched but rewrote to the literal x instead of "hi". The substitution-spot visitor used expr.acceptChildrenVoid(visitor), which only walks expr's CHILDREN. For an after body like { x -> x }, expr IS the IrGetValue — it has no children, so the visitor never registered it and the template rendered the source slice unchanged. Fix: expr.accept(visitor, null) instead.

New end-to-end test RecipePluginRewriteTest."chain with Java-static inner segment — Optional_of_x_get to x" exercises all three fixes.

Test plan

  • :rewrite-kotlin:test — all 1205+ tests green locally.
  • Tested downstream in moderneinc/recipes-kotlin Performance and Interop recipe families against the rebuilt 8.82.0-SNAPSHOT. Unblocks xs.map(f).toMutableList() → xs.mapTo(mutableListOf(), f) (and the filterTo/filterNotTo/flatMapTo siblings) and Optional.of(x).get() → x.

…ng lambda is present

`preserveTrailingLambdaShape` was unconditionally adding `OmitParentheses`
to the args container whenever the last arg was a `TrailingLambdaArgument`-
marked lambda. For multi-arg calls (`obj.method(arg, lambda)`),
`OmitParentheses` tells the Kotlin printer to drop ALL the call's parens
— so the call rendered as `obj.methodarg { lambda }` with no `(` after
the method name and no `)` before the lambda.

Gate `OmitParentheses` on `padded.size() == 1` (single-lambda-only form).
For multi-arg trailing-lambda calls, the `TrailingLambdaArgument` marker
on the lambda is the only signal needed: `KotlinPrinter.visitArgumentsContainer`
already emits `(` ... `)` around the non-lambda args and the lambda
outside.

Unblocks `xs.map(f).toMutableList()` -> `xs.mapTo(mutableListOf(), f)`
and the rest of the `mapTo`/`filterTo`/`flatMapTo` family.
…fter bodies

Two fixes in `RecipeIrGenerationExtension` that landed together because
the second only surfaces once the first is in place — verified by a new
end-to-end test `chain with Java-static inner segment — Optional_of_x_get
to x`.

1. Chain validator rejected Java-static inner segments. K2 wraps Java-
   platform-typed call results (`Optional<T>!` -> `Optional<T>`) in an
   `IrTypeOperatorCall(IMPLICIT_CAST)`. The chain detector's
   `rawReceiver is IrCall` check was false because `rawReceiver` was the
   type-op wrapper, not the wrapped `IrCall`. Added
   `unwrapImplicitCasts(expr)` that peels `IMPLICIT_CAST` / `IMPLICIT_NOTNULL`
   wrappers and call it at every `IrCall`-shape-check site in
   `validateBeforeLambda` (root receiver, inner receiver, inner args,
   outer args, root args).

2. `buildAfterTemplate` missed bare-param-reference after bodies.
   With (1) in place, `Optional.of("hi").get()` matched but the rewrite
   emitted the literal `x` instead of `"hi"`. The substitution-spot
   visitor was driven by `expr.acceptChildrenVoid(visitor)`, which only
   walks expr's CHILDREN. For an after body that's a single
   `{ x -> x }`, expr IS the `IrGetValue` — it has no children, so the
   visitor never registered it as a substitution spot and the template
   rendered the source slice unchanged. Switched to
   `expr.accept(visitor, null)` so the visitor sees expr itself first.

Unblocks `Optional.of(x).get() -> x` style chain rewrites where the
inner segment is a Java static call (the same shape covers
`Stream.of(x).findFirst()`, `List.of(x).getFirst()`, etc.).
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite May 17, 2026
@jkschneider jkschneider merged commit 4045ad6 into main May 17, 2026
1 check passed
@jkschneider jkschneider deleted the kotlin-munich branch May 17, 2026 21:16
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 17, 2026
@jkschneider jkschneider restored the kotlin-munich branch May 18, 2026 13:07
@jkschneider jkschneider deleted the kotlin-munich branch May 18, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant