Skip to content

Commit b80a19b

Browse files
committed
Widen concrete HashMap declared type when UseMapOf rewrites it to Map.of (#1148)
1 parent ceb4af5 commit b80a19b

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

src/main/java/org/openrewrite/java/migrate/util/UseMapOf.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package org.openrewrite.java.migrate.util;
1717

1818
import lombok.Getter;
19+
import org.jspecify.annotations.Nullable;
20+
import org.openrewrite.Cursor;
1921
import org.openrewrite.ExecutionContext;
2022
import org.openrewrite.Preconditions;
2123
import org.openrewrite.Recipe;
@@ -29,7 +31,9 @@
2931
import org.openrewrite.java.search.UsesMethod;
3032
import org.openrewrite.java.tree.Expression;
3133
import org.openrewrite.java.tree.J;
34+
import org.openrewrite.java.tree.JavaType;
3235
import org.openrewrite.java.tree.Statement;
36+
import org.openrewrite.java.tree.TypeTree;
3337
import org.openrewrite.java.tree.TypeUtils;
3438

3539
import java.util.ArrayList;
@@ -194,6 +198,94 @@ private J reattachPairPrefixes(J applied, List<J.MethodInvocation> puts, boolean
194198
return nc.withArguments(Collections.singletonList(mapCall.withArguments(withPrefixes)));
195199
}
196200

201+
@Override
202+
public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
203+
// The anonymous-class form replaces `new HashMap<>() {{ put(..); }}` with an immutable
204+
// `Map.of(..)`/`Map.ofEntries(..)`, which is not assignable to a concrete `HashMap`
205+
// declared type. When such an initializer will be rewritten, widen the declared type to
206+
// `Map<..>` and swap it onto the cursor before visiting the initializer, so the generated
207+
// `Map.of(..)` is type-attributed against the `Map` LHS rather than `HashMap` (#1148).
208+
J.VariableDeclarations widened = widenRewritableHashMapDeclaration(multiVariable);
209+
if (widened != null) {
210+
setCursor(new Cursor(getCursor().getParent(), widened));
211+
return super.visitVariableDeclarations(widened, ctx);
212+
}
213+
return super.visitVariableDeclarations(multiVariable, ctx);
214+
}
215+
216+
/**
217+
* If {@code decl} is a single-variable, concrete {@code HashMap} declaration whose anonymous
218+
* {@code new HashMap<>() {{ put(..); }}} initializer would be rewritten to an immutable
219+
* {@code Map.of(..)}, returns a copy with the declared type widened to {@code java.util.Map}
220+
* (type expression plus the variable's own type attribution); {@code null} otherwise.
221+
*/
222+
private J.@Nullable VariableDeclarations widenRewritableHashMapDeclaration(J.VariableDeclarations decl) {
223+
if (decl.getVariables().size() != 1 ||
224+
!TypeUtils.isOfClassType(decl.getType(), "java.util.HashMap")) {
225+
return null;
226+
}
227+
Expression initializer = decl.getVariables().get(0).getInitializer();
228+
if (!(initializer instanceof J.NewClass) || !rewritesToImmutableMapOf((J.NewClass) initializer)) {
229+
return null;
230+
}
231+
232+
JavaType.FullyQualified mapType = JavaType.ShallowClass.build("java.util.Map");
233+
TypeTree typeExpression = decl.getTypeExpression();
234+
TypeTree widenedExpression;
235+
JavaType widenedType;
236+
if (typeExpression instanceof J.ParameterizedType) {
237+
J.ParameterizedType pt = (J.ParameterizedType) typeExpression;
238+
if (!(pt.getClazz() instanceof J.Identifier)) {
239+
return null;
240+
}
241+
widenedType = pt.getType() instanceof JavaType.Parameterized ?
242+
((JavaType.Parameterized) pt.getType()).withType(mapType) : mapType;
243+
J.Identifier clazz = ((J.Identifier) pt.getClazz()).withSimpleName("Map").withType(mapType);
244+
widenedExpression = pt.withClazz(clazz).withType(widenedType);
245+
} else if (typeExpression instanceof J.Identifier) {
246+
widenedType = mapType;
247+
widenedExpression = ((J.Identifier) typeExpression).withSimpleName("Map").withType(mapType);
248+
} else {
249+
return null;
250+
}
251+
252+
J.VariableDeclarations.NamedVariable nv = decl.getVariables().get(0);
253+
JavaType.Variable variableType = nv.getVariableType();
254+
if (variableType != null) {
255+
variableType = variableType.withType(widenedType);
256+
nv = nv.withVariableType(variableType).withName(nv.getName().withType(widenedType).withFieldType(variableType));
257+
}
258+
return decl.withTypeExpression(widenedExpression).withVariables(Collections.singletonList(nv));
259+
}
260+
261+
/**
262+
* Mirrors the anonymous-class guard in {@link #visitNewClass}: returns {@code true} when
263+
* {@code nc} is a {@code new HashMap<>() {{ put(k, v); ... }}} whose body holds only
264+
* non-null {@code put(..)} calls, and so will be replaced with an immutable {@code Map.of(..)}.
265+
*/
266+
private boolean rewritesToImmutableMapOf(J.NewClass nc) {
267+
J.Block body = nc.getBody();
268+
if (!NEW_HASH_MAP.matches(nc) || body == null || body.getStatements().size() != 1 ||
269+
!TypeUtils.isOfClassType(nc.getClazz() != null ? nc.getClazz().getType() : null, "java.util.HashMap")) {
270+
return false;
271+
}
272+
Statement statement = body.getStatements().get(0);
273+
if (!(statement instanceof J.Block)) {
274+
return false;
275+
}
276+
for (Statement stat : ((J.Block) statement).getStatements()) {
277+
if (!(stat instanceof J.MethodInvocation) || !MAP_PUT.matches((Expression) stat)) {
278+
return false;
279+
}
280+
for (Expression arg : ((J.MethodInvocation) stat).getArguments()) {
281+
if (J.Literal.isLiteralValue(arg, null)) {
282+
return false;
283+
}
284+
}
285+
}
286+
return true;
287+
}
288+
197289
@Override
198290
public J visitBlock(J.Block block, ExecutionContext ctx) {
199291
Map<UUID, List<J.MethodInvocation>> rewrites = new HashMap<>();

src/test/java/org/openrewrite/java/migrate/util/UseMapOfTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,66 @@ class Test {
205205
);
206206
}
207207

208+
@Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/1148")
209+
@Test
210+
void widenConcreteHashMapFieldTypeToMap() {
211+
//language=java
212+
rewriteRun(
213+
spec -> spec.allSources(s -> s.markers(javaVersion(25))),
214+
java(
215+
"""
216+
import java.util.HashMap;
217+
218+
class Main {
219+
private static final HashMap<String, String> VALUES = new HashMap<String, String>() {{
220+
put("key", "value");
221+
}};
222+
}
223+
""",
224+
"""
225+
import java.util.Map;
226+
227+
class Main {
228+
private static final Map<String, String> VALUES = Map.of("key", "value");
229+
}
230+
"""
231+
)
232+
);
233+
}
234+
235+
@Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/1148")
236+
@Test
237+
void widenConcreteHashMapLocalVariableTypeToMap() {
238+
//language=java
239+
rewriteRun(
240+
java(
241+
"""
242+
import java.util.HashMap;
243+
244+
class Main {
245+
void m() {
246+
HashMap<String, Integer> ages = new HashMap<>() {{
247+
put("Bob", 42);
248+
put("alice", 30);
249+
}};
250+
}
251+
}
252+
""",
253+
"""
254+
import java.util.Map;
255+
256+
class Main {
257+
void m() {
258+
Map<String, Integer> ages = Map.of(
259+
"Bob", 42,
260+
"alice", 30);
261+
}
262+
}
263+
"""
264+
)
265+
);
266+
}
267+
208268
@Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/1112")
209269
@Test
210270
void doNotChangeLinkedHashMap() {

0 commit comments

Comments
 (0)