Skip to content

Add ExplicitTypeToVar recipe to replace explicit type with var keyword.#1009

Open
motlin wants to merge 2 commits intoopenrewrite:mainfrom
motlin:ExplicitTypeToVar
Open

Add ExplicitTypeToVar recipe to replace explicit type with var keyword.#1009
motlin wants to merge 2 commits intoopenrewrite:mainfrom
motlin:ExplicitTypeToVar

Conversation

@motlin
Copy link

@motlin motlin commented Mar 12, 2026

What's changed?

Add a new ExplicitTypeToVar recipe that replaces explicit type declarations with var when the variable is initialized with a constructor call of exactly the same type.

For example: ArrayList<String> list = new ArrayList<>()var list = new ArrayList<String>()

The recipe transfers type arguments from the declaration to the constructor (replacing diamond operators) so the type remains unambiguous after the transformation.

What's your motivation?

The existing UseVarForObject and UseVarForGenericsConstructors recipes are broader and may transform declarations where the declared type differs from the constructor type (e.g., List<String> list = new ArrayList<>()). This recipe takes a more conservative approach, only transforming when the declared type exactly matches the constructor type, making it safer for incremental var adoption.

Anything in particular you'd like reviewers to focus on?

  • The type-argument transfer logic in maybeTransferTypeArguments — ensures diamond operators on the constructor get replaced with the explicit type parameters from the declaration so the var transformation doesn't lose type information.
  • Whether the DeclarationCheck.isVarApplicable and DeclarationCheck.transformToVar utilities cover all edge cases for this recipe's scope.

Anyone you would like to review specifically?

Have you considered any alternatives or workarounds?

A configurable option on the UseVarForObject / UseVarForGenericsConstructors recipes to restrict to exact-match only — decided a separate recipe is cleaner and easier to compose.

Any additional context

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've used the IntelliJ IDEA auto-formatter on affected files

@timtebeek
Copy link
Member

At-scale validation results

Ran the ExplicitTypeToVar recipe against the Default organization (11 repos) using the Moderne CLI.

Results summary

Repository Changes
finos/spring-bot 227
finos/symphony-bdk-java 369
finos/symphony-wdk 257
spring-projects/spring-data-commons 282
spring-projects/spring-petclinic 32
Netflix/photon 0
Netflix/ribbon 0
apache/maven-doxia 0
finos/messageml-utils 0

~1,167 total transformations across 5 repositories.

Correctness checks

  • No false positives — no interface-to-impl type mismatches (e.g. List x = new ArrayList<>() was correctly skipped)
  • Diamond operator transfer — type arguments correctly transferred from declaration to constructor (e.g. HashMap<String, Integer> x = new HashMap<>()var x = new HashMap<String, Integer>())
  • final modifier preservedfinal HashMap<String, Integer> becomes final var as expected
  • Multi-line constructor calls handled — e.g. TreeMap<String, JsonNode> with a multi-line comparator argument
  • Non-constructor initializers skipped — factory methods, interface types, etc. correctly left unchanged

Sample transformations

Simple:

-  StringBuilder manipulated = new StringBuilder(cl.getCanonicalName());
+  var manipulated = new StringBuilder(cl.getCanonicalName());

Diamond operator transfer:

-  final HashMap<String, Integer> argumentIndexes = new HashMap<>();
+  final var argumentIndexes = new HashMap<String, Integer>();

Nested generics:

-  AtomicReference<ResultPair<String>> result = new AtomicReference<>();
+  var result = new AtomicReference<ResultPair<String>>();

Multi-line:

-  TreeMap<String, JsonNode> errorsByLocation =
-      new TreeMap<>(Comparator.comparingInt((String s) -> StringUtils.countMatches(s, '/'))
+  var errorsByLocation =
+      new TreeMap<String, JsonNode>(Comparator.comparingInt((String s) -> StringUtils.countMatches(s, '/'))
           .thenComparing(Function.identity()));

@timtebeek timtebeek added the recipe Recipe requested label Mar 12, 2026
@timtebeek
Copy link
Member

Comparison: UseVarForGenericsConstructors vs ExplicitTypeToVar

Scope

UseVarForGenericsConstructors ExplicitTypeToVar
Target Generic constructor calls only Any constructor call
Type match Does NOT check declared type == constructor type Requires TypeUtils.isOfType exact match
Primitives Explicitly skips Handled by DeclarationCheck.isVarApplicable
Ternaries Explicitly skips Not checked (relies on isVarApplicable)
Generics required Yes — skips non-generic declarations No — handles both generic and non-generic
Wildcard bounds Explicitly skips (? extends, ? super) Not checked
Interface vs impl Not checked — will transform List<String> x = new ArrayList<>() Rejects — declared type must exactly match constructor type

Key Behavioral Differences

  1. UseVarForGenericsConstructors is broader on type mismatches: It will transform List<String> x = new ArrayList<>()var x = new ArrayList<String>(), even though ListArrayList. ExplicitTypeToVar would skip this because the types don't match exactly.

  2. ExplicitTypeToVar is broader on non-generics: It handles StringBuilder sb = new StringBuilder()var sb = new StringBuilder(). UseVarForGenericsConstructors skips non-generic declarations entirely.

  3. ExplicitTypeToVar is more conservative overall: The exact type match requirement means it won't accidentally widen or narrow the inferred type. If you declare Map<K,V> m = new HashMap<>(), it won't transform because Map ≠ HashMap.

Type Parameter Transfer (diamond → explicit)

Both transfer type parameters from the LHS to the RHS when the constructor uses a diamond operator (<>), but ExplicitTypeToVar.maybeTransferTypeArguments is more thorough — it checks for J.Empty instances inside the type parameter list (diamond operator representation), while UseVarForGenericsConstructors checks if rightTypes (extracted JavaTypes) is empty.

Structural Differences

  • UseVarForGenericsConstructors uses @Getter on individual fields; ExplicitTypeToVar uses @Value on the class
  • UseVarForGenericsConstructors has ~60 lines more code due to type parameter extraction helpers and bounds checking
  • Both share DeclarationCheck.isVarApplicable and DeclarationCheck.transformToVar

Overlap

For generic constructor calls where the declared type exactly matches the constructor type (e.g., ArrayList<String> x = new ArrayList<>()), both recipes would produce the same transformation. ExplicitTypeToVar is a strict subset of UseVarForGenericsConstructors in the generic case, plus it additionally covers non-generic exact-match constructors.

@timtebeek
Copy link
Member

I like the mostly widened scope here as compared to the UseVarForGenericsConstructors we already had, but perhaps we don't need both necessarily. I do wonder if it makes sense to covert cases like List<String> x = new ArrayList<>() too, although there's a small risk there in changing exposed types when it's a field with a Lombok getter.

If we are to merge this one I do think we should probably remove or deprecate and redirect UseVarForGenericsConstructors, as to not cause confusion which to use when. Might even change the name of this one to UseVarForConstructors to align with other similar recipes already.

What are your thoughts on the above?

@motlin
Copy link
Author

motlin commented Mar 16, 2026

@timtebeek I personally wouldn't want List<String> x = new ArrayList<>() transformed to var x = new ArrayList<String>() in my codebase because the type of x switches from the interface to the concrete type, and then I'd be allowed to called public, non-interface methods like ensureCapacity() and trimToSize().

I prefer var only where the type doesn't change and it's very obvious what the type is. That includes constructor calls which is what this recipe does. I agree with you about the rename, this should probably be called UseVarForConstructors. The other place that I use var is on literals, like var s = "obviously a string" which this recipe does not handle.

@timtebeek
Copy link
Member

That makes sense, thanks; Indeed best then to keep the logic as it is, or make it opt-in to change interface to concrete type.

@motlin motlin force-pushed the ExplicitTypeToVar branch from 6021842 to 4300f9a Compare March 17, 2026 16:35
@motlin
Copy link
Author

motlin commented Mar 17, 2026

I renamed to UseVarForConstructors. I think it's ready to go?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

recipe Recipe requested

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants