Skip to content

Kotlin recipe DSL: preserve dot-on-its-own-line layout across chain collapse#7729

Merged
jkschneider merged 1 commit into
mainfrom
kotlin-munich
May 19, 2026
Merged

Kotlin recipe DSL: preserve dot-on-its-own-line layout across chain collapse#7729
jkschneider merged 1 commit into
mainfrom
kotlin-munich

Conversation

@jkschneider
Copy link
Copy Markdown
Member

@jkschneider jkschneider commented May 19, 2026

Summary

Real-world Kotlin chains are formatted with each .member(...) on its own indented line:

xs
    .filter { p }
    .firstOrNull()

The K2 plugin's chain-collapse rewrite synthesizes a single fused call (xs.firstOrNull(p)) by applying a KotlinTemplate at the matched outer's coordinates. The result's outer-call select.after defaults to "" — the whitespace that used to sit between the inner's text end and the outer's . is lost — and the synthesized .firstOrNull(p) ends up jammed onto the previous line. Surfaced by Performance / BestPractices chain-collapse recipes in moderneinc/recipes-kotlin running against real corpora.

preserveSelectAfter copies the matched outer call's select.after onto the template-substituted result so the new outer sits on its own dot-prefixed line, matching where the matched outer originally was.

Wired into every method-invocation rewrite path so chain-shape and bare-shape rewrites both preserve the author's line layout:

  • methodInvocationRewrite (bare Kotlin)
  • methodInvocationRewriteJava (Java template)
  • methodInvocationRewriteKotlinNotNull (K.Unary unwrap)
  • chainMethodInvocationRewrite (two-segment chain)

No-op when the matched outer has no select.after whitespace (already single-line) or when the result has no select (template produced a non-method shape).

Test plan

Four new tests in RecipePluginRewriteTest:

  • chain collapse preserves dot-on-its-own-line layout — positive case for the chain path (filter(p).firstOrNull() collapse with multi-line layout in the source)
  • chain collapse — single-line input stays single-line (no false-positive newline) — negative case proving the empty-whitespace guard doesn't insert newlines into single-line chains
  • chain collapse — alternate shape (map then filterNotNull) also preserves layout — second chain-shape variant proving the fix isn't specific to filter/firstOrNull (this is the shape that surfaced the bug)
  • bare single-call rewrite preserves dot-on-its-own-line layout — exercises the non-chain methodInvocationRewrite path so we don't regress one branch while fixing another

./gradlew :rewrite-kotlin:test green.

…ollapse

Real-world Kotlin chains are formatted with each `.member(...)` on its own
indented line:

    xs
        .filter { p }
        .firstOrNull()

The K2 plugin's chain-collapse rewrite synthesizes a single fused call
(`xs.firstOrNull(p)`) by applying a Kotlin template at the matched outer's
coordinates. The result's outer-call `select.after` defaults to "" — the
whitespace that used to sit between the inner's text end and the outer's
"." is lost — and the synthesized `.firstOrNull(p)` ends up jammed onto
the previous line:

    xs
        .filter { p }.firstOrNull { p }   // before this fix

`preserveSelectAfter` copies the matched outer call's `select.after`
onto the template-substituted result so the new outer sits on its own
dot-prefixed line, matching where the matched outer originally was:

    xs
        .filter { p }
        .firstOrNull { p }                // after this fix

Wired into every method-invocation rewrite path:
- `methodInvocationRewrite` (bare Kotlin)
- `methodInvocationRewriteJava` (Java template)
- `methodInvocationRewriteKotlinNotNull` (K.Unary unwrap)
- `chainMethodInvocationRewrite` (the two-segment chain path)

No-op when the matched outer has no `select.after` whitespace (already
single-line) or when the result has no select (template produced a
non-method shape).

Surfaced by Performance / BestPractices chain-collapse recipes in
moderneinc/recipes-kotlin running against real corpora.
@jkschneider jkschneider merged commit 1f7a368 into main May 19, 2026
1 check passed
@jkschneider jkschneider deleted the kotlin-munich branch May 19, 2026 01:54
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant