2020import org .openrewrite .Preconditions ;
2121import org .openrewrite .Recipe ;
2222import org .openrewrite .TreeVisitor ;
23+ import org .openrewrite .internal .ListUtils ;
24+ import org .openrewrite .java .JavaIsoVisitor ;
2325import org .openrewrite .java .JavaTemplate ;
2426import org .openrewrite .java .JavaVisitor ;
2527import org .openrewrite .java .MethodMatcher ;
3032import org .openrewrite .java .tree .Statement ;
3133
3234import java .util .ArrayList ;
35+ import java .util .Collections ;
36+ import java .util .HashMap ;
37+ import java .util .HashSet ;
3338import java .util .List ;
39+ import java .util .Map ;
40+ import java .util .Set ;
3441import java .util .StringJoiner ;
42+ import java .util .UUID ;
43+ import java .util .concurrent .atomic .AtomicBoolean ;
3544
3645public class UseListOf extends Recipe {
3746 private static final MethodMatcher NEW_ARRAY_LIST = new MethodMatcher ("java.util.ArrayList <constructor>()" , true );
3847 private static final MethodMatcher LIST_ADD = new MethodMatcher ("java.util.List add(..)" , true );
3948
49+ private static final String PROSE_REWRITES_KEY = "use-list-of.prose-rewrites" ;
50+
4051 @ Getter
4152 final String displayName = "Prefer `List.of(..)`" ;
4253
4354 @ Getter
44- final String description = "Prefer `List.of(..)` instead of using `java.util.List#add(..)` in anonymous ArrayList initializers in Java 10 or higher. " +
45- "This recipe will not modify code where the List is later mutated since `List.of` returns an immutable list." ;
55+ final String description = "Prefer `List.of(..)` in Java 10 or higher. Two input shapes are recognised:\n \n " +
56+ "- Anonymous-class initialization (`new ArrayList<>() {{ add(\" a\" ); add(\" b\" ); }}`), " +
57+ "which is replaced wholesale with `List.of(\" a\" , \" b\" )` (immutable result, matching the " +
58+ "anonymous-class idiom's typical intent).\n " +
59+ "- A `new ArrayList<>()` declaration followed by a chain of `target.add(..)` statements, " +
60+ "which is collapsed to `new ArrayList<>(List.of(..))` (preserving the mutable `ArrayList`)." ;
4661
4762 @ Override
4863 public TreeVisitor <?, ExecutionContext > getVisitor () {
@@ -54,6 +69,32 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
5469 @ Override
5570 public J visitNewClass (J .NewClass newClass , ExecutionContext ctx ) {
5671 J .NewClass n = (J .NewClass ) super .visitNewClass (newClass , ctx );
72+
73+ // Prose-pattern: see if visitBlock (above us on the cursor) decided this
74+ // initializer should be wrapped with `new ArrayList<>(List.of(..))`.
75+ Map <UUID , List <J .MethodInvocation >> rewrites = getCursor ().getNearestMessage (PROSE_REWRITES_KEY );
76+ if (rewrites != null ) {
77+ List <J .MethodInvocation > adds = rewrites .get (n .getId ());
78+ if (adds != null ) {
79+ List <Expression > args = new ArrayList <>();
80+ StringJoiner joiner = new StringJoiner (", " , "new ArrayList<>(List.of(" , "))" );
81+ for (J .MethodInvocation add : adds ) {
82+ args .add (add .getArguments ().get (0 ));
83+ joiner .add ("#{any()}" );
84+ }
85+ maybeAddImport ("java.util.List" );
86+ J applied = JavaTemplate .builder (joiner .toString ())
87+ .contextSensitive ()
88+ .imports ("java.util.ArrayList" , "java.util.List" )
89+ .build ()
90+ .apply (updateCursor (n ), n .getCoordinates ().replace (), args .toArray ());
91+ // Reattach each add's prefix so the elements land one-per-line and any
92+ // leading comments survive, then autoformat to nest the indentation.
93+ return autoFormat (reattachElementPrefixes (applied , adds ), ctx );
94+ }
95+ }
96+
97+ // Anonymous-class form (original UseListOf logic, unchanged).
5798 J .Block body = n .getBody ();
5899 if (NEW_ARRAY_LIST .matches (n ) && body != null && body .getStatements ().size () == 1 ) {
59100 Statement statement = body .getStatements ().get (0 );
@@ -65,7 +106,6 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
65106 return n ;
66107 }
67108 J .MethodInvocation add = (J .MethodInvocation ) stat ;
68- // List.add() takes only one argument
69109 if (add .getArguments ().size () != 1 ) {
70110 return n ;
71111 }
@@ -85,7 +125,175 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
85125
86126 return n ;
87127 }
128+
129+ /**
130+ * Re-applies the absorbed add statements' prefixes to the generated
131+ * {@code new ArrayList<>(List.of(..))} so each element keeps its own line and any
132+ * leading comments. {@code adds} holds one invocation per element, in order.
133+ */
134+ private J reattachElementPrefixes (J applied , List <J .MethodInvocation > adds ) {
135+ if (!(applied instanceof J .NewClass )) {
136+ return applied ;
137+ }
138+ J .NewClass nc = (J .NewClass ) applied ;
139+ if (nc .getArguments ().size () != 1 || !(nc .getArguments ().get (0 ) instanceof J .MethodInvocation )) {
140+ return applied ;
141+ }
142+ J .MethodInvocation listCall = (J .MethodInvocation ) nc .getArguments ().get (0 );
143+ List <Expression > listArgs = listCall .getArguments ();
144+ List <Expression > withPrefixes = new ArrayList <>(listArgs .size ());
145+ for (int i = 0 ; i < listArgs .size (); i ++) {
146+ withPrefixes .add (listArgs .get (i ).withPrefix (adds .get (i ).getPrefix ()));
147+ }
148+ return nc .withArguments (Collections .singletonList (listCall .withArguments (withPrefixes )));
149+ }
150+
151+ @ Override
152+ public J visitBlock (J .Block block , ExecutionContext ctx ) {
153+ // Pre-pass: scan the ORIGINAL block to identify which initializers to
154+ // rewrite and which `add(..)` statements to absorb. UUIDs are stable
155+ // through super.visitBlock unless a child visitor rebuilds the node,
156+ // and nothing else in this recipe touches the targeted initializers
157+ // before visitNewClass fires.
158+ Map <UUID , List <J .MethodInvocation >> rewrites = new HashMap <>();
159+ Set <UUID > absorbedAddIds = new HashSet <>();
160+ identifyProseRewrites (block , rewrites , absorbedAddIds );
161+
162+ if (!rewrites .isEmpty ()) {
163+ getCursor ().putMessage (PROSE_REWRITES_KEY , rewrites );
164+ }
165+
166+ J .Block b = (J .Block ) super .visitBlock (block , ctx );
167+
168+ // Post-pass: drop the now-absorbed `add(..)` statements from the block.
169+ return b .withStatements (ListUtils .filter (b .getStatements (), s -> !absorbedAddIds .contains (s .getId ())));
170+ }
171+
172+ /**
173+ * Walk the block's statements looking for:
174+ * <pre>
175+ * List<T> name = new ArrayList<>();
176+ * name.add(x1);
177+ * name.add(x2);
178+ * ...
179+ * </pre>
180+ * For each such sequence with at least two adds, record
181+ * (initializer UUID, the absorbed {@code add(..)} invocations in order) in
182+ * {@code rewrites} and the absorbed add statement UUIDs in {@code absorbedAddIds}.
183+ */
184+ private void identifyProseRewrites (
185+ J .Block block ,
186+ Map <UUID , List <J .MethodInvocation >> rewrites ,
187+ Set <UUID > absorbedAddIds ) {
188+ List <Statement > stmts = block .getStatements ();
189+ int i = 0 ;
190+ while (i < stmts .size ()) {
191+ Statement stmt = stmts .get (i );
192+ if (!(stmt instanceof J .VariableDeclarations )) {
193+ i ++;
194+ continue ;
195+ }
196+ J .VariableDeclarations decl = (J .VariableDeclarations ) stmt ;
197+ String targetName = matchingTargetName (decl );
198+ if (targetName == null ) {
199+ i ++;
200+ continue ;
201+ }
202+ J .NewClass initializer = (J .NewClass ) decl .getVariables ().get (0 ).getInitializer ();
203+ // (matchingTargetName already verified the initializer is a J.NewClass)
204+
205+ List <J .MethodInvocation > adds = new ArrayList <>();
206+ List <UUID > absorbedHere = new ArrayList <>();
207+ int j = i + 1 ;
208+ while (j < stmts .size ()) {
209+ Statement next = stmts .get (j );
210+ Expression arg = matchAddCallOn (next , targetName );
211+ if (arg == null || expressionReferences (arg , targetName )) {
212+ break ;
213+ }
214+ adds .add ((J .MethodInvocation ) next );
215+ absorbedHere .add (next .getId ());
216+ j ++;
217+ }
218+ if (adds .size () >= 2 && initializer != null ) {
219+ rewrites .put (initializer .getId (), adds );
220+ absorbedAddIds .addAll (absorbedHere );
221+ i = j ;
222+ } else {
223+ i ++;
224+ }
225+ }
226+ }
227+
228+ /**
229+ * Returns the variable name if {@code decl} is a single-variable, parameterized
230+ * {@code List<T>} declaration whose initializer is a no-arg {@code new ArrayList<>()}
231+ * with no anonymous-class body. Returns {@code null} otherwise.
232+ */
233+ private String matchingTargetName (J .VariableDeclarations decl ) {
234+ if (decl .getVariables ().size () != 1 ) {
235+ return null ;
236+ }
237+ // Require parameterized LHS; for raw `List` we'd be guessing at a type argument.
238+ if (!(decl .getTypeExpression () instanceof J .ParameterizedType )) {
239+ return null ;
240+ }
241+ J .VariableDeclarations .NamedVariable nv = decl .getVariables ().get (0 );
242+ if (!(nv .getInitializer () instanceof J .NewClass )) {
243+ return null ;
244+ }
245+ J .NewClass nc = (J .NewClass ) nv .getInitializer ();
246+ if (!NEW_ARRAY_LIST .matches (nc )) {
247+ return null ;
248+ }
249+ // A body would put us in the anonymous-class case handled by visitNewClass directly.
250+ if (nc .getBody () != null ) {
251+ return null ;
252+ }
253+ return nv .getSimpleName ();
254+ }
255+
256+ /**
257+ * If {@code stmt} is {@code targetName.add(arg)} matching {@link #LIST_ADD},
258+ * returns the single argument expression; otherwise {@code null}. Also returns
259+ * {@code null} when the argument is the {@code null} literal, since
260+ * {@code List.of(..)} rejects nulls.
261+ */
262+ private Expression matchAddCallOn (Statement stmt , String targetName ) {
263+ if (!(stmt instanceof J .MethodInvocation )) {
264+ return null ;
265+ }
266+ J .MethodInvocation mi = (J .MethodInvocation ) stmt ;
267+ if (!LIST_ADD .matches (mi )) {
268+ return null ;
269+ }
270+ if (mi .getArguments ().size () != 1 ) {
271+ return null ;
272+ }
273+ if (!(mi .getSelect () instanceof J .Identifier )) {
274+ return null ;
275+ }
276+ if (!targetName .equals (((J .Identifier ) mi .getSelect ()).getSimpleName ())) {
277+ return null ;
278+ }
279+ Expression arg = mi .getArguments ().get (0 );
280+ if (arg instanceof J .Literal && ((J .Literal ) arg ).getValue () == null ) {
281+ return null ;
282+ }
283+ return arg ;
284+ }
285+
286+ private boolean expressionReferences (Expression expr , String name ) {
287+ return new JavaIsoVisitor <AtomicBoolean >() {
288+ @ Override
289+ public J .Identifier visitIdentifier (J .Identifier id , AtomicBoolean f ) {
290+ if (name .equals (id .getSimpleName ())) {
291+ f .set (true );
292+ }
293+ return id ;
294+ }
295+ }.reduce (expr , new AtomicBoolean (false )).get ();
296+ }
88297 });
89298 }
90-
91299}
0 commit comments