|
16 | 16 | package org.openrewrite.java.migrate.util; |
17 | 17 |
|
18 | 18 | import lombok.Getter; |
| 19 | +import org.jspecify.annotations.Nullable; |
| 20 | +import org.openrewrite.Cursor; |
19 | 21 | import org.openrewrite.ExecutionContext; |
20 | 22 | import org.openrewrite.Preconditions; |
21 | 23 | import org.openrewrite.Recipe; |
|
29 | 31 | import org.openrewrite.java.search.UsesMethod; |
30 | 32 | import org.openrewrite.java.tree.Expression; |
31 | 33 | import org.openrewrite.java.tree.J; |
| 34 | +import org.openrewrite.java.tree.JavaType; |
32 | 35 | import org.openrewrite.java.tree.Statement; |
| 36 | +import org.openrewrite.java.tree.TypeTree; |
33 | 37 | import org.openrewrite.java.tree.TypeUtils; |
34 | 38 |
|
35 | 39 | import java.util.ArrayList; |
@@ -194,6 +198,94 @@ private J reattachPairPrefixes(J applied, List<J.MethodInvocation> puts, boolean |
194 | 198 | return nc.withArguments(Collections.singletonList(mapCall.withArguments(withPrefixes))); |
195 | 199 | } |
196 | 200 |
|
| 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 | + |
197 | 289 | @Override |
198 | 290 | public J visitBlock(J.Block block, ExecutionContext ctx) { |
199 | 291 | Map<UUID, List<J.MethodInvocation>> rewrites = new HashMap<>(); |
|
0 commit comments