Skip to content

rewrite-kotlin: variadic-by-default matching in the recipe DSL#7895

Merged
jkschneider merged 1 commit into
mainfrom
variadic-args
Jun 3, 2026
Merged

rewrite-kotlin: variadic-by-default matching in the recipe DSL#7895
jkschneider merged 1 commit into
mainfrom
variadic-args

Conversation

@jkschneider

@jkschneider jkschneider commented Jun 3, 2026

Copy link
Copy Markdown
Member

What

Teaches the rewrite { } to { } recipe DSL to treat varargs methods as variadic by default. When the before pattern targets a method whose declared last parameter is varargs, the args the author places in that slot become a representative group rather than a literal count — so the recipe matches call sites of any arity and carries them all through. The author writes the natural shape; no *args ceremony required.

// matches Arrays.asList(...) of any arity (2, 4, even 0 -> List.of())
rewrite { a: Any, b: Any, c: Any -> java.util.Arrays.asList(a, b, c) } to { a, b, c -> java.util.List.of(a, b, c) }

// natural String.format -> String.formatted (JDK 15+)
rewrite { fmt: String, a: Any, b: Any -> java.lang.String.format(fmt, a, b) } to { fmt, a, b -> fmt.formatted(a, b) }

The variadic group can be repositioned in the after template — the motivating slf4j case moves a trailing Throwable from before the varargs run to after them (log(msg, t, params…)error(msg, params…, t)). An explicit .strictArity() modifier opts out into exact-arity matching, and the explicit spread form { args: Array<Any> -> asList(*args) } is also supported.

How

  • Matcher: a varargs callee is always matched with prefix…, .. — a MethodMatcher matches the declared signature, where varargs is a single array param, so a fixed (*,*,*) spec can never match it. Fixed prefix params are typed (Kotlin builtins → Java FQN) to avoid sibling-overload matches like String.format(Locale, String, Object...).
  • After template: carries a control-char sentinel + a V<k> substitution source. At apply time the runtime expands the sentinel to one #{any()} per matched arg and splices getArguments()[k..end] (the canonical zero-arg [J.Empty] collapses to count 0). Placeholders bind positionally, so in-place expansion keeps template and substitution array aligned. This handles pass-through, reorder, and .strictArity() (a runtime exact-count guard) uniformly.
  • Surface: to now returns a RewriteRule so (rewrite { } to { }).strictArity() chains off it; the FIR checker accepts strictArity as a valid rewrite terminal.

Bonus: the mapped-type fallback customizer now preserves varargs

Testing

New cases in RecipePluginRewriteTest (compile-and-run via the K2 plugin + rewriteRun): by-example pass-through (Java + Kotlin sources), explicit spread, String.formatformatted, the slf4j reorder, and .strictArity(). RecipeMappedTypeFallbackTest updated to assert the natural formatted("x", "y") call. Full :rewrite-kotlin:test is green.

Teach the `rewrite { } to { }` recipe DSL to handle varargs methods as
varargs by default. When the before pattern targets a method whose declared
last parameter is varargs, the args the author places in that slot become a
representative group rather than a literal count, so the recipe matches call
sites of any arity and carries them all through:

    rewrite { a: Any, b: Any, c: Any -> java.util.Arrays.asList(a, b, c) } to
        { a, b, c -> java.util.List.of(a, b, c) }

matches asList(...) of any arity (2, 4, even 0). The group can be repositioned
in the after template (the slf4j case: move a trailing Throwable from before
the varargs run to after them), and an explicit `.strictArity()` modifier opts
out into exact-arity matching. The spread form `{ args -> asList(*args) }` is
also accepted.

Mechanics:
- The matcher for a varargs callee is always `prefix…, ..` (a MethodMatcher
  only matches the declared single array param, so `(*,*,*)` never would),
  with typed prefix params to avoid sibling-overload matches.
- The after template carries a sentinel + `V<k>` substitution source; the
  runtime expands it to one `#{any()}` per matched arg and splices
  getArguments()[k..end] (the canonical zero-arg `[J.Empty]` collapses to 0).
- `to` now returns a `RewriteRule` so `.strictArity()` chains off it; the FIR
  checker accepts it as a valid rewrite terminal.

Also fix the mapped-type fallback customizer to PRESERVE varargs: re-exposed
Java varargs methods like `String.formatted(Object...)` now surface as a real
Kotlin `vararg args: Any?` (out-projected `Array<out Any?>` + isVararg=true)
instead of a plain `Array<Any?>`. The prior invariant-array form tripped
`IllegalStateException: Vararg expression has incorrect type` in fir2ir; the
out projection resolves it. So `formatted` is a first-class variadic callee and
`format(fmt, a, b) -> fmt.formatted(a, b)` works as natural by-example.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Jun 3, 2026
@jkschneider
jkschneider merged commit e8d1fac into main Jun 3, 2026
1 check passed
@jkschneider
jkschneider deleted the variadic-args branch June 3, 2026 19:43
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Jun 3, 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