From 9c01db17deaf6a1202dd96d4b819eb2f7a66b91d Mon Sep 17 00:00:00 2001 From: "Craig P. Motlin" Date: Thu, 12 Mar 2026 00:03:32 -0400 Subject: [PATCH 1/4] Add ExplicitTypeToVar recipe to replace explicit type with var keyword. --- .../migrate/lang/var/ExplicitTypeToVar.java | 122 ++++++++++++ .../resources/META-INF/rewrite/recipes.csv | 1 + .../lang/var/ExplicitTypeToVarTest.java | 181 ++++++++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java create mode 100644 src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java b/src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java new file mode 100644 index 0000000000..72457aaa7a --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java @@ -0,0 +1,122 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang.var; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.*; + +import java.util.List; + +/** + * Replaces explicit type declarations with {@code var} keyword when the initializer + * is a constructor call with an exactly matching type. + * + *

This recipe is more conservative than {@link UseVarForObject} and + * {@link UseVarForGenericsConstructors}. It only transforms when the declared type + * exactly matches the constructor type, avoiding cases where the declared type is an + * interface or supertype. + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class ExplicitTypeToVar extends Recipe { + + String displayName = "Explicit type to `var` for exact constructor matches"; + + String description = "Replace explicit type declarations with `var` when the variable is initialized with a " + + "constructor call of exactly the same type. Does not transform when declared type " + + "differs from constructor type (e.g., interface vs implementation)."; + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesJavaVersion<>(10), + new ExplicitTypeToVarVisitor()); + } + + static final class ExplicitTypeToVarVisitor extends JavaIsoVisitor { + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations vd, ExecutionContext ctx) { + vd = super.visitVariableDeclarations(vd, ctx); + + if (!DeclarationCheck.isVarApplicable(getCursor(), vd)) { + return vd; + } + + Expression initializer = vd.getVariables().get(0).getInitializer(); + if (initializer == null) { + return vd; + } + initializer = initializer.unwrap(); + + // Only transform constructor calls + if (!(initializer instanceof J.NewClass)) { + return vd; + } + + // Declared type must exactly match constructor type + if (!TypeUtils.isOfType(vd.getType(), initializer.getType())) { + return vd; + } + + if (vd.getType() instanceof JavaType.FullyQualified) { + maybeRemoveImport((JavaType.FullyQualified) vd.getType()); + } + + J.VariableDeclarations finalVd = vd; + return DeclarationCheck.transformToVar(vd, it -> maybeTransferTypeArguments(finalVd, it)); + } + + private static J.NewClass maybeTransferTypeArguments(J.VariableDeclarations vd, J.NewClass initializer) { + TypeTree typeExpression = vd.getTypeExpression(); + + if (!(typeExpression instanceof J.ParameterizedType)) { + return initializer; + } + J.ParameterizedType paramType = (J.ParameterizedType) typeExpression; + + List declaredTypeParams = paramType.getTypeParameters(); + if (declaredTypeParams == null || declaredTypeParams.isEmpty()) { + return initializer; + } + + TypeTree constructorClazz = initializer.getClazz(); + if (!(constructorClazz instanceof J.ParameterizedType)) { + return initializer; + } + J.ParameterizedType constructorParamType = (J.ParameterizedType) constructorClazz; + + List constructorTypeParams = constructorParamType.getTypeParameters(); + if (constructorTypeParams == null || isDiamondOperator(constructorTypeParams)) { + J.ParameterizedType newClazz = constructorParamType.withTypeParameters(declaredTypeParams); + return initializer.withClazz(newClazz); + } + + return initializer; + } + + private static boolean isDiamondOperator(List typeParams) { + return typeParams.isEmpty() || typeParams.stream().allMatch(J.Empty.class::isInstance); + } + } +} diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 4bee6c6837..7e04b4a0c1 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -336,6 +336,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.l maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.UseVar,Use local variable type inference,"Apply local variable type inference (`var`) for primitives and objects. These recipes can cause unused imports, be advised to run `org.openrewrite.java.RemoveUnusedImports afterwards.",5,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.FindVirtualThreadOpportunities,Find Virtual Thread opportunities,Find opportunities to convert existing code to use Virtual Threads.,10,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.table.MethodCalls"",""displayName"":""Method calls"",""description"":""The text of matching method invocations."",""columns"":[{""name"":""sourceFile"",""type"":""String"",""displayName"":""Source file"",""description"":""The source file that the method call occurred in.""},{""name"":""method"",""type"":""String"",""displayName"":""Method call"",""description"":""The text of the method call.""},{""name"":""className"",""type"":""String"",""displayName"":""Class name"",""description"":""The class name of the method call.""},{""name"":""methodName"",""type"":""String"",""displayName"":""Method name"",""description"":""The method name of the method call.""},{""name"":""argumentTypes"",""type"":""String"",""displayName"":""Argument types"",""description"":""The argument types of the method call.""}]}]" maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.FindNonVirtualExecutors,Find non-virtual `ExecutorService` creation,Find all places where static `java.util.concurrent.Executors` method creates a non-virtual `java.util.concurrent.ExecutorService`. This recipe can be used to search fro `ExecutorService` that can be replaced by Virtual Thread executor.,7,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.table.MethodCalls"",""displayName"":""Method calls"",""description"":""The text of matching method invocations."",""columns"":[{""name"":""sourceFile"",""type"":""String"",""displayName"":""Source file"",""description"":""The source file that the method call occurred in.""},{""name"":""method"",""type"":""String"",""displayName"":""Method call"",""description"":""The text of the method call.""},{""name"":""className"",""type"":""String"",""displayName"":""Class name"",""description"":""The class name of the method call.""},{""name"":""methodName"",""type"":""String"",""displayName"":""Method name"",""description"":""The method name of the method call.""},{""name"":""argumentTypes"",""type"":""String"",""displayName"":""Argument types"",""description"":""The argument types of the method call.""}]}]" +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.ExplicitTypeToVar,Explicit type to `var` for exact constructor matches,Replace explicit type declarations with `var` when the variable is initialized with a constructor call of exactly the same type. Does not transform when declared type differs from constructor type (e.g. interface vs implementation).,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForGenericMethodInvocations,Apply `var` to generic method invocations,"Apply `var` to variables initialized by invocations of generic methods. This recipe ignores generic factory methods without parameters, because open rewrite cannot handle them correctly ATM.",1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForGenericsConstructors,Apply `var` to Generic Constructors,Apply `var` to generics variables initialized by constructor calls.,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForObject,Use `var` for reference-typed variables,Try to apply local variable type inference `var` to variables containing Objects where possible. This recipe will not touch variable declarations with generics or initializers containing ternary operators.,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java b/src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java new file mode 100644 index 0000000000..f91e1a48df --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang.var; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class ExplicitTypeToVarTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ExplicitTypeToVar()) + .allSources(s -> s.markers(javaVersion(17))); + } + + @DocumentExample + @Test + void replacePatterns() { + rewriteRun( + //language=java + java( + """ + import java.util.ArrayList; + import java.util.HashMap; + + class Test { + void test() { + // Basic constructor + StringBuilder sb = new StringBuilder(); + + // Constructor with arguments + StringBuilder sbWithArg = new StringBuilder("initial"); + + // Generics with concrete types + ArrayList list = new ArrayList<>(); + + // Nested generics with concrete types + HashMap> map = new HashMap<>(); + + // Type variable in generic + ArrayList typeVarList = new ArrayList<>(); + + // Multiple type variables + HashMap typeVarMap = new HashMap<>(); + + // Nested type variables + HashMap> nested = new HashMap<>(); + + // In lambda + Runnable r = () -> { + ArrayList lambdaList = new ArrayList<>(); + }; + } + + // Instance initializer + { + StringBuilder initSb = new StringBuilder(); + } + + // Static initializer + static { + StringBuilder staticSb = new StringBuilder(); + } + } + """, + """ + import java.util.ArrayList; + import java.util.HashMap; + + class Test { + void test() { + // Basic constructor + var sb = new StringBuilder(); + + // Constructor with arguments + var sbWithArg = new StringBuilder("initial"); + + // Generics with concrete types + var list = new ArrayList(); + + // Nested generics with concrete types + var map = new HashMap>(); + + // Type variable in generic + var typeVarList = new ArrayList(); + + // Multiple type variables + var typeVarMap = new HashMap(); + + // Nested type variables + var nested = new HashMap>(); + + // In lambda + Runnable r = () -> { + var lambdaList = new ArrayList(); + }; + } + + // Instance initializer + { + var initSb = new StringBuilder(); + } + + // Static initializer + static { + var staticSb = new StringBuilder(); + } + } + """ + ) + ); + } + + @Test + void doNotReplaceInvalidPatterns() { + rewriteRun( + //language=java + java( + """ + import java.util.ArrayList; + import java.util.List; + + class Test { + // Field declarations + private final ArrayList privateField = new ArrayList<>(); + protected final ArrayList protectedField = new ArrayList<>(); + public final ArrayList publicField = new ArrayList<>(); + final ArrayList packageField = new ArrayList<>(); + ArrayList nonFinalField = new ArrayList<>(); + + void test() { + // Interface vs implementation + List list = new ArrayList<>(); + + // Supertype + Object obj = new StringBuilder(); + + // Already using var + var existing = new ArrayList<>(); + + // No initializer + ArrayList noInit; + noInit = new ArrayList<>(); + + // Not a constructor call + ArrayList fromFactory = getList(); + + // Multiple variables + String a = "a", b = "b"; + + // Null initializer + StringBuilder nullInit = null; + } + + ArrayList getList() { + return new ArrayList<>(); + } + } + """ + ) + ); + } +} From 4300f9ae9f9c66e6020374bbee9a6ecaf1eb4d80 Mon Sep 17 00:00:00 2001 From: "Craig P. Motlin" Date: Tue, 17 Mar 2026 10:54:53 -0400 Subject: [PATCH 2/4] Rename ExplicitTypeToVar to UseVarForConstructors. --- ...{ExplicitTypeToVar.java => UseVarForConstructors.java} | 8 ++++---- src/main/resources/META-INF/rewrite/recipes.csv | 3 ++- ...tTypeToVarTest.java => UseVarForConstructorsTest.java} | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) rename src/main/java/org/openrewrite/java/migrate/lang/var/{ExplicitTypeToVar.java => UseVarForConstructors.java} (94%) rename src/test/java/org/openrewrite/java/migrate/lang/var/{ExplicitTypeToVarTest.java => UseVarForConstructorsTest.java} (98%) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java similarity index 94% rename from src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java rename to src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java index 72457aaa7a..94d49dc10f 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVar.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java @@ -38,9 +38,9 @@ */ @Value @EqualsAndHashCode(callSuper = false) -public class ExplicitTypeToVar extends Recipe { +public class UseVarForConstructors extends Recipe { - String displayName = "Explicit type to `var` for exact constructor matches"; + String displayName = "Use `var` for constructor call assignments"; String description = "Replace explicit type declarations with `var` when the variable is initialized with a " + "constructor call of exactly the same type. Does not transform when declared type " + @@ -50,10 +50,10 @@ public class ExplicitTypeToVar extends Recipe { public TreeVisitor getVisitor() { return Preconditions.check( new UsesJavaVersion<>(10), - new ExplicitTypeToVarVisitor()); + new UseVarForConstructorsVisitor()); } - static final class ExplicitTypeToVarVisitor extends JavaIsoVisitor { + static final class UseVarForConstructorsVisitor extends JavaIsoVisitor { @Override public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations vd, ExecutionContext ctx) { diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 7e04b4a0c1..dc8751fc3c 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -336,7 +336,8 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.l maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.UseVar,Use local variable type inference,"Apply local variable type inference (`var`) for primitives and objects. These recipes can cause unused imports, be advised to run `org.openrewrite.java.RemoveUnusedImports afterwards.",5,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.FindVirtualThreadOpportunities,Find Virtual Thread opportunities,Find opportunities to convert existing code to use Virtual Threads.,10,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.table.MethodCalls"",""displayName"":""Method calls"",""description"":""The text of matching method invocations."",""columns"":[{""name"":""sourceFile"",""type"":""String"",""displayName"":""Source file"",""description"":""The source file that the method call occurred in.""},{""name"":""method"",""type"":""String"",""displayName"":""Method call"",""description"":""The text of the method call.""},{""name"":""className"",""type"":""String"",""displayName"":""Class name"",""description"":""The class name of the method call.""},{""name"":""methodName"",""type"":""String"",""displayName"":""Method name"",""description"":""The method name of the method call.""},{""name"":""argumentTypes"",""type"":""String"",""displayName"":""Argument types"",""description"":""The argument types of the method call.""}]}]" maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.FindNonVirtualExecutors,Find non-virtual `ExecutorService` creation,Find all places where static `java.util.concurrent.Executors` method creates a non-virtual `java.util.concurrent.ExecutorService`. This recipe can be used to search fro `ExecutorService` that can be replaced by Virtual Thread executor.,7,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.table.MethodCalls"",""displayName"":""Method calls"",""description"":""The text of matching method invocations."",""columns"":[{""name"":""sourceFile"",""type"":""String"",""displayName"":""Source file"",""description"":""The source file that the method call occurred in.""},{""name"":""method"",""type"":""String"",""displayName"":""Method call"",""description"":""The text of the method call.""},{""name"":""className"",""type"":""String"",""displayName"":""Class name"",""description"":""The class name of the method call.""},{""name"":""methodName"",""type"":""String"",""displayName"":""Method name"",""description"":""The method name of the method call.""},{""name"":""argumentTypes"",""type"":""String"",""displayName"":""Argument types"",""description"":""The argument types of the method call.""}]}]" -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.ExplicitTypeToVar,Explicit type to `var` for exact constructor matches,Replace explicit type declarations with `var` when the variable is initialized with a constructor call of exactly the same type. Does not transform when declared type differs from constructor type (e.g. interface vs implementation).,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForConstructors,Use `var` for constructor call assignments,Replace explicit type declarations with `var` when the variable is initialized with a constructor call of exactly the same type. Does not transform when declared type differs from constructor type (e.g. interface vs implementation).,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForLiterals,Use `var` for literal assignments,Replace explicit type declarations with `var` when the variable is initialized with a String literal. Primitive literals are handled by `UseVarForPrimitive`.,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForGenericMethodInvocations,Apply `var` to generic method invocations,"Apply `var` to variables initialized by invocations of generic methods. This recipe ignores generic factory methods without parameters, because open rewrite cannot handle them correctly ATM.",1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForGenericsConstructors,Apply `var` to Generic Constructors,Apply `var` to generics variables initialized by constructor calls.,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.var.UseVarForObject,Use `var` for reference-typed variables,Try to apply local variable type inference `var` to variables containing Objects where possible. This recipe will not touch variable declarations with generics or initializers containing ternary operators.,1,Var,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java similarity index 98% rename from src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java rename to src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java index f91e1a48df..bafd2cc62a 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/var/ExplicitTypeToVarTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java @@ -23,11 +23,11 @@ import static org.openrewrite.java.Assertions.java; import static org.openrewrite.java.Assertions.javaVersion; -class ExplicitTypeToVarTest implements RewriteTest { +class UseVarForConstructorsTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.recipe(new ExplicitTypeToVar()) + spec.recipe(new UseVarForConstructors()) .allSources(s -> s.markers(javaVersion(17))); } From e8ccdb06203f852663a04a8c480fdf08bf468daa Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 21 Mar 2026 22:09:59 +0100 Subject: [PATCH 3/4] Polish --- .../lang/var/UseVarForConstructors.java | 122 ++++++++---------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java index 94d49dc10f..2acba29fa2 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructors.java @@ -17,6 +17,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import org.jspecify.annotations.Nullable; import org.openrewrite.ExecutionContext; import org.openrewrite.Preconditions; import org.openrewrite.Recipe; @@ -48,75 +49,66 @@ public class UseVarForConstructors extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check( - new UsesJavaVersion<>(10), - new UseVarForConstructorsVisitor()); - } - - static final class UseVarForConstructorsVisitor extends JavaIsoVisitor { - - @Override - public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations vd, ExecutionContext ctx) { - vd = super.visitVariableDeclarations(vd, ctx); - - if (!DeclarationCheck.isVarApplicable(getCursor(), vd)) { - return vd; - } - - Expression initializer = vd.getVariables().get(0).getInitializer(); - if (initializer == null) { - return vd; - } - initializer = initializer.unwrap(); - - // Only transform constructor calls - if (!(initializer instanceof J.NewClass)) { - return vd; - } - - // Declared type must exactly match constructor type - if (!TypeUtils.isOfType(vd.getType(), initializer.getType())) { - return vd; - } - - if (vd.getType() instanceof JavaType.FullyQualified) { - maybeRemoveImport((JavaType.FullyQualified) vd.getType()); + return Preconditions.check(new UsesJavaVersion<>(10), new JavaIsoVisitor() { + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations varDecl, ExecutionContext ctx) { + J.VariableDeclarations vd = super.visitVariableDeclarations(varDecl, ctx); + + if (!DeclarationCheck.isVarApplicable(getCursor(), vd)) { + return vd; + } + + Expression initializer = vd.getVariables().get(0).getInitializer(); + if (initializer == null) { + return vd; + } + initializer = initializer.unwrap(); + + // Only transform constructor calls + if (!(initializer instanceof J.NewClass)) { + return vd; + } + + // Declared type must exactly match constructor type, as to not have additional methods become available + if (!TypeUtils.isOfType(vd.getType(), initializer.getType())) { + return vd; + } + + if (vd.getType() instanceof JavaType.FullyQualified) { + maybeRemoveImport((JavaType.FullyQualified) vd.getType()); + } + + return DeclarationCheck.transformToVar(vd, (J.NewClass nc) -> maybeTransferTypeArguments(vd, nc)); } - J.VariableDeclarations finalVd = vd; - return DeclarationCheck.transformToVar(vd, it -> maybeTransferTypeArguments(finalVd, it)); - } + private J.NewClass maybeTransferTypeArguments(J.VariableDeclarations vd, J.NewClass initializer) { + TypeTree typeExpression = vd.getTypeExpression(); + if (!(typeExpression instanceof J.ParameterizedType)) { + return initializer; + } + J.ParameterizedType paramType = (J.ParameterizedType) typeExpression; + + List declaredTypeParams = paramType.getTypeParameters(); + if (declaredTypeParams == null || declaredTypeParams.isEmpty()) { + return initializer; + } + + TypeTree constructorClazz = initializer.getClazz(); + if (!(constructorClazz instanceof J.ParameterizedType)) { + return initializer; + } + J.ParameterizedType constructorParamType = (J.ParameterizedType) constructorClazz; + + List constructorTypeParams = constructorParamType.getTypeParameters(); + boolean nullEmptyOrDiamondOperator = constructorTypeParams == null || + constructorTypeParams.isEmpty() || + constructorTypeParams.stream().allMatch(J.Empty.class::isInstance); + if (nullEmptyOrDiamondOperator) { + return initializer.withClazz(constructorParamType.withTypeParameters(declaredTypeParams)); + } - private static J.NewClass maybeTransferTypeArguments(J.VariableDeclarations vd, J.NewClass initializer) { - TypeTree typeExpression = vd.getTypeExpression(); - - if (!(typeExpression instanceof J.ParameterizedType)) { - return initializer; - } - J.ParameterizedType paramType = (J.ParameterizedType) typeExpression; - - List declaredTypeParams = paramType.getTypeParameters(); - if (declaredTypeParams == null || declaredTypeParams.isEmpty()) { return initializer; } - - TypeTree constructorClazz = initializer.getClazz(); - if (!(constructorClazz instanceof J.ParameterizedType)) { - return initializer; - } - J.ParameterizedType constructorParamType = (J.ParameterizedType) constructorClazz; - - List constructorTypeParams = constructorParamType.getTypeParameters(); - if (constructorTypeParams == null || isDiamondOperator(constructorTypeParams)) { - J.ParameterizedType newClazz = constructorParamType.withTypeParameters(declaredTypeParams); - return initializer.withClazz(newClazz); - } - - return initializer; - } - - private static boolean isDiamondOperator(List typeParams) { - return typeParams.isEmpty() || typeParams.stream().allMatch(J.Empty.class::isInstance); - } + }); } } From 343063408eb1e8b49f619529839e08f87fbf3fa1 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 21 Mar 2026 22:21:17 +0100 Subject: [PATCH 4/4] Additional test cases --- .../lang/var/UseVarForConstructorsTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java index bafd2cc62a..ee347b0bfd 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForConstructorsTest.java @@ -38,6 +38,7 @@ void replacePatterns() { //language=java java( """ + import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.HashMap; @@ -49,9 +50,15 @@ void test() { // Constructor with arguments StringBuilder sbWithArg = new StringBuilder("initial"); + // Final modifier + final StringBuilder finalSb = new StringBuilder(); + // Generics with concrete types ArrayList list = new ArrayList<>(); + // Explicit type arguments on constructor (non-diamond) + ArrayList explicitList = new ArrayList(); + // Nested generics with concrete types HashMap> map = new HashMap<>(); @@ -64,10 +71,22 @@ void test() { // Nested type variables HashMap> nested = new HashMap<>(); + // Inner class constructor + HashMap.SimpleEntry entry = new HashMap.SimpleEntry<>("key", 1); + // In lambda Runnable r = () -> { ArrayList lambdaList = new ArrayList<>(); }; + + // For-loop initializer + for (StringBuilder forSb = new StringBuilder(); forSb.length() < 10; forSb.append("x")) { + } + + // Try-with-resources + try (ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0])) { + } catch (Exception e) { + } } // Instance initializer @@ -82,6 +101,7 @@ void test() { } """, """ + import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.HashMap; @@ -93,9 +113,15 @@ void test() { // Constructor with arguments var sbWithArg = new StringBuilder("initial"); + // Final modifier + final var finalSb = new StringBuilder(); + // Generics with concrete types var list = new ArrayList(); + // Explicit type arguments on constructor (non-diamond) + var explicitList = new ArrayList(); + // Nested generics with concrete types var map = new HashMap>(); @@ -108,10 +134,22 @@ void test() { // Nested type variables var nested = new HashMap>(); + // Inner class constructor + var entry = new HashMap.SimpleEntry("key", 1); + // In lambda Runnable r = () -> { var lambdaList = new ArrayList(); }; + + // For-loop initializer + for (var forSb = new StringBuilder(); forSb.length() < 10; forSb.append("x")) { + } + + // Try-with-resources + try (var bais = new ByteArrayInputStream(new byte[0])) { + } catch (Exception e) { + } } // Instance initializer @@ -129,6 +167,23 @@ void test() { ); } + @Test + void doNotReplaceWhenJavaVersionBelow10() { + rewriteRun( + spec -> spec.allSources(s -> s.markers(javaVersion(9))), + //language=java + java( + """ + class Test { + void test() { + StringBuilder sb = new StringBuilder(); + } + } + """ + ) + ); + } + @Test void doNotReplaceInvalidPatterns() { rewriteRun( @@ -168,6 +223,13 @@ void test() { // Null initializer StringBuilder nullInit = null; + + // Anonymous inner class + ArrayList anonList = new ArrayList() {}; + Runnable r = new Runnable() { + @Override + public void run() {} + }; } ArrayList getList() {