Skip to content

Extend Use{List,Set,Map}Of to recognise prose-statement chains#1145

Merged
timtebeek merged 4 commits into
mainfrom
extend-use-list-set-map-of-prose-pattern
Jun 25, 2026
Merged

Extend Use{List,Set,Map}Of to recognise prose-statement chains#1145
timtebeek merged 4 commits into
mainfrom
extend-use-list-set-map-of-prose-pattern

Conversation

@steve-aom-elliott

Copy link
Copy Markdown
Contributor

Summary

UseListOf, UseSetOf, and UseMapOf previously matched only the anonymous-class idiom (new ArrayList<>() {{ add("a"); add("b"); }}). Each recipe now also recognises the much more common prose-statement shape:

// before
List<String> names = new ArrayList<>();
names.add("Bob");
names.add("alice");
names.add("Charlie");

// after
List<String> names = new ArrayList<>(List.of("Bob", "alice", "Charlie"));

The same pattern applies to Set<T>/HashSet (with Set.of(...)) and Map<K,V>/HashMap (with Map.of(k, v, ...) up to 10 pairs, then Map.ofEntries(Map.entry(k, v), ...)).

The output is always wrapped in the mutable ArrayList/HashSet/HashMap so 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)

Recipe Constructor Method Output factory
UseListOf new ArrayList<>() target.add(arg) new ArrayList<>(List.of(...))
UseSetOf new HashSet<>() target.add(arg) new HashSet<>(Set.of(...))
UseMapOf new HashMap<>() target.put(k, v) new HashMap<>(Map.of(...)) / new HashMap<>(Map.ofEntries(...)) past 10 pairs

Bail conditions

Each recipe leaves code unchanged when any of the following applies:

  • The LHS is a raw type (e.g. List names = new ArrayList()) — we'd be guessing at a type argument. Other recipes can be composed to parameterise first.
  • The constructor takes arguments (e.g. new ArrayList<>(10) with a capacity hint) — silently dropping the hint would be misleading.
  • Any statement between the declaration and a later add/put isn't itself a recognised call on the same target (e.g. System.out.println(...), addAll(..)).
  • An argument references the target variable itself (e.g. target.add(target.size())) — the collapsed List.of(...) would evaluate against the pre-add state and change semantics.
  • Any argument is the null literal — List.of / Set.of / Map.of / Map.entry all reject null at runtime.
  • Fewer than two add/put statements follow the declaration — the rewrite would be noise.

Implementation

A visitBlock pre-pass scans for prose patterns and records (initializer UUID → [args]) on a cursor message. super.visitBlock then visits children; the existing visitNewClass override consults the message via getCursor().getNearestMessage(...) and, when matched, applies a JavaTemplate at the correct cursor depth (where updateCursor(n) is well-defined). After super.visitBlock returns, a post-pass filters the now-absorbed add/put statements 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 to UseListOfTest.
  • UseMapOfTest — 12 existing + 6 new: positive Map.of, Map.ofEntries boundary at 11 pairs, single-put below threshold, intervening statement, value-referencing-target, null value, raw LHS.
  • ./gradlew test passes (full suite, no cross-package regressions).
  • ./gradlew recipeCsvValidate passes; recipes.csv regenerated to reflect the new descriptions.

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.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Jun 24, 2026
@steve-aom-elliott steve-aom-elliott added enhancement New feature or request recipe Recipe requested test provided labels Jun 24, 2026
@steve-aom-elliott steve-aom-elliott moved this from In Progress to Ready to Review in OpenRewrite Jun 24, 2026

@timtebeek timtebeek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@timtebeek timtebeek merged commit ceb4af5 into main Jun 25, 2026
1 check passed
@timtebeek timtebeek deleted the extend-use-list-set-map-of-prose-pattern branch June 25, 2026 10:57
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request recipe Recipe requested test provided

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants