Extend Use{List,Set,Map}Of to recognise prose-statement chains - #1145
Merged
Conversation
UseListOf, UseSetOf, and UseMapOf previously matched only the
anonymous-class idiom:
List<String> l = new ArrayList<>() {{ add("a"); add("b"); }};
Extends each recipe to also recognise the more common prose-statement
shape, where a no-arg constructor declaration is followed by a chain
of add(..) or put(..) calls on the declared variable:
List<String> l = new ArrayList<>();
l.add("a");
l.add("b");
These are collapsed into the constructor with the immutable factory:
List<String> l = new ArrayList<>(List.of("a", "b"));
Output is always wrapped in the mutable ArrayList/HashSet/HashMap so
downstream mutations (.sort(), .add(), etc.) remain valid.
Recognised shapes per recipe:
- UseListOf: target.add(arg) chains; output uses List.of(..).
- UseSetOf: target.add(arg) chains; output uses Set.of(..).
- UseMapOf: target.put(k, v) chains; output uses Map.of(k, v, ..)
up to 10 pairs, Map.ofEntries(Map.entry(k, v), ..) beyond.
Bail conditions (all three): raw LHS, non-no-arg constructor (e.g.
capacity hint), intervening non-recognised statement, an argument
that references the target variable (depends on step-by-step
mutation), null literal argument. Threshold: at least 2 add/put
statements; a single call is not worth the rewrite churn.
Implementation: a visitBlock pre-pass scans for prose patterns and
records (initializer UUID -> [args]) on a cursor message; the
existing visitNewClass override consults the map and applies the
JavaTemplate at the correct cursor depth. After super.visitBlock,
the post-pass filters absorbed add/put statements from the block.
Regenerate recipes.csv to reflect the new descriptions.
timtebeek
approved these changes
Jun 25, 2026
timtebeek
left a comment
Member
There was a problem hiding this comment.
Nice extension indeed! Much more common case. Still iterating locally on some readability improvements for map pairs, as all on a single line can hurt readability I think.
mergify Bot
added a commit
to robfrank/linklift
that referenced
this pull request
Jul 17, 2026
…37.0 to 3.40.0 [skip ci] Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 3.37.0 to 3.40.0. Release notes *Sourced from [org.openrewrite.recipe:rewrite-migrate-java's releases](https://github.com/openrewrite/rewrite-migrate-java/releases).* > 3.40.0 > ------ > > What's Changed > -------------- > > * Keep `\t` and `\f` escapes in UseTextBlocks to avoid incidental whitespace stripping by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1160](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1160) > * Skip UseEnumSetOf empty conversion for static fields to avoid circular class-init ([#1157](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1157)) by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1161](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1161) > * Skip `UseMapOf` for `LinkedHashMap`/`TreeMap` in the prose `put()` form ([#1163](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1163)) by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1165](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1165) > * Skip AddMockitoJavaAgentToMavenSurefirePlugin when agent already configured in build/plugins by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1166](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1166) > * Add RedundantUtf8Charset recipe to drop redundant UTF-8 from Files calls by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1167](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1167) > * Fix Jakarta EE 10 web.xml migration by [`@sakshichitnis27`](https://github.com/sakshichitnis27) in [openrewrite/rewrite-migrate-java#1159](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1159) > > New Contributors > ---------------- > > * [`@sakshichitnis27`](https://github.com/sakshichitnis27) made their first contribution in [openrewrite/rewrite-migrate-java#1159](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1159) > > **Full Changelog**: <openrewrite/rewrite-migrate-java@v3.39.0...v3.40.0> > > 3.39.0 > ------ > > What's Changed > -------------- > > * Make ExtractExplicitConstructorInvocationArguments templates context-free by [`@knutwannheden`](https://github.com/knutwannheden) in [openrewrite/rewrite-migrate-java#1140](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1140) > * Convert Javadoc `<br>` tags to Markdown line breaks by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1142](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1142) > * Preserve angle brackets when converting `@param <T>` to Markdown by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1141](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1141) > * Fix JavadocToMarkdown's handling of varargs and JDKs newer than the bytecode source/target compatibility by [`@sambsnyd`](https://github.com/sambsnyd) in [openrewrite/rewrite-migrate-java#1143](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1143) > * Move ReplaceUnusedVariablesWithUnderscore to Java best practices by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1144](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1144) > * Extend Use{List,Set,Map}Of to recognise prose-statement chains by [`@steve-aom-elliott`](https://github.com/steve-aom-elliott) in [openrewrite/rewrite-migrate-java#1145](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1145) > * Skip `UseMapOf` when the anonymous `HashMap` is assigned to a concrete `HashMap` variable by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1149](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1149) > * Fix jakarta.enterprise.concurrent-api version for EE9 and EE10 by [`@evie-lau`](https://github.com/evie-lau) in [openrewrite/rewrite-migrate-java#1152](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1152) > * Add recipe to replace jakarta jws-api with ws-api in EE10 by [`@evie-lau`](https://github.com/evie-lau) in [openrewrite/rewrite-migrate-java#1151](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1151) > * Upgrade maven-ejb-plugin alongside jakarta.ejb-api by [`@evie-lau`](https://github.com/evie-lau) in [openrewrite/rewrite-migrate-java#1155](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1155) > * Upgrade maven-ear-plugin for ee9 and ee11 by [`@evie-lau`](https://github.com/evie-lau) in [openrewrite/rewrite-migrate-java#1156](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1156) > > **Full Changelog**: <openrewrite/rewrite-migrate-java@v3.38.0...v3.39.0> > > v3.38.0 > ------- > > What's Changed > -------------- > > * use . instead # for FQN link in Markdown javadoc by [`@barbulescu`](https://github.com/barbulescu) in [openrewrite/rewrite-migrate-java#1134](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1134) > * Bump maven-pmd-plugin to 3.28.x for Java 25 support by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1136](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1136) > * Explain why Kotlin modules are capped at Java 24 during Java 25 migration by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1137](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1137) > * Preserve `@{argLine}` in AddSurefireFailsafeArgLine for JaCoCo coverage by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1138](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1138) > * Add recipe to extract complex explicit constructor invocation arguments (JEP 513) by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1135](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1135) > * Bump Kotlin 2.0-2.2 to 2.3 so Kotlin modules can reach Java 25 by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-migrate-java#1139](https://redirect.github.com/openrewrite/rewrite-migrate-java/pull/1139) > > **Full Changelog**: <openrewrite/rewrite-migrate-java@v3.37.0...v3.38.0> Commits * [`6584812`](openrewrite/rewrite-migrate-java@6584812) Fix Jakarta EE 10 web.xml migration ([#1159](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1159)) * [`3d0ad8f`](openrewrite/rewrite-migrate-java@3d0ad8f) OpenRewrite recipe best practices * [`76925b9`](openrewrite/rewrite-migrate-java@76925b9) Add RedundantUtf8Charset recipe to drop redundant UTF-8 from Files calls ([#1167](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1167)) * [`c0d0f67`](openrewrite/rewrite-migrate-java@c0d0f67) Skip AddMockitoJavaAgentToMavenSurefirePlugin when agent already configured i... * [`12386c8`](openrewrite/rewrite-migrate-java@12386c8) Skip UseMapOf for LinkedHashMap/TreeMap in prose put-statement form ([#1163](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1163)) (... * [`ee2f4c4`](openrewrite/rewrite-migrate-java@ee2f4c4) OpenRewrite recipe best practices * [`5f18964`](openrewrite/rewrite-migrate-java@5f18964) Skip UseEnumSetOf empty conversion for static fields to avoid circular class-... * [`4cd2123`](openrewrite/rewrite-migrate-java@4cd2123) Keep \t and \f escapes in UseTextBlocks to avoid incidental whitespace stripp... * [`16e662d`](openrewrite/rewrite-migrate-java@16e662d) Upgrade maven-ear-plugin for ee9 and ee11 ([#1156](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1156)) * [`b9f0360`](openrewrite/rewrite-migrate-java@b9f0360) Upgrade maven-ejb-plugin alongside jakarta.ejb-api ([#1155](https://redirect.github.com/openrewrite/rewrite-migrate-java/issues/1155)) * Additional commits viewable in [compare view](openrewrite/rewrite-migrate-java@v3.37.0...v3.40.0)
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.
Summary
UseListOf,UseSetOf, andUseMapOfpreviously matched only the anonymous-class idiom (new ArrayList<>() {{ add("a"); add("b"); }}). Each recipe now also recognises the much more common prose-statement shape:The same pattern applies to
Set<T>/HashSet(withSet.of(...)) andMap<K,V>/HashMap(withMap.of(k, v, ...)up to 10 pairs, thenMap.ofEntries(Map.entry(k, v), ...)).The output is always wrapped in the mutable
ArrayList/HashSet/HashMapso any downstream.sort(),.add(), etc. remain valid. This is the safe choice in all cases; a future enhancement could detect "never mutated after the chain" and shed the wrap.Recognised shapes (per recipe)
UseListOfnew ArrayList<>()target.add(arg)new ArrayList<>(List.of(...))UseSetOfnew HashSet<>()target.add(arg)new HashSet<>(Set.of(...))UseMapOfnew HashMap<>()target.put(k, v)new HashMap<>(Map.of(...))/new HashMap<>(Map.ofEntries(...))past 10 pairsBail conditions
Each recipe leaves code unchanged when any of the following applies:
List names = new ArrayList()) — we'd be guessing at a type argument. Other recipes can be composed to parameterise first.new ArrayList<>(10)with a capacity hint) — silently dropping the hint would be misleading.add/putisn't itself a recognised call on the same target (e.g.System.out.println(...),addAll(..)).target.add(target.size())) — the collapsedList.of(...)would evaluate against the pre-add state and change semantics.nullliteral —List.of/Set.of/Map.of/Map.entryall reject null at runtime.add/putstatements follow the declaration — the rewrite would be noise.Implementation
A
visitBlockpre-pass scans for prose patterns and records(initializer UUID → [args])on a cursor message.super.visitBlockthen visits children; the existingvisitNewClassoverride consults the message viagetCursor().getNearestMessage(...)and, when matched, applies aJavaTemplateat the correct cursor depth (whereupdateCursor(n)is well-defined). Aftersuper.visitBlockreturns, a post-pass filters the now-absorbedadd/putstatements out of the block.The original anonymous-class logic is preserved verbatim — both shapes are handled by the same recipe class.
Test plan
UseListOfTest— 7 existing + 7 new: positive prose-chain, single-add below threshold, intervening statement, arg-referencing-target, null arg, raw LHS, capacity-hint constructor.UseSetOfTest— 8 existing + 6 new: parallel coverage toUseListOfTest.UseMapOfTest— 12 existing + 6 new: positiveMap.of,Map.ofEntriesboundary at 11 pairs, single-put below threshold, intervening statement, value-referencing-target, null value, raw LHS../gradlew testpasses (full suite, no cross-package regressions)../gradlew recipeCsvValidatepasses;recipes.csvregenerated to reflect the new descriptions.