From b1f55a83d4a2b5dc67c9a99d0bb9365ac2b84ea5 Mon Sep 17 00:00:00 2001 From: martinfrancois Date: Mon, 10 Aug 2026 09:44:49 +0200 Subject: [PATCH 1/3] PrimitiveWrapperClassConstructorToValueOf: preserve Float(double) rounding `new Float()` was retyped to a String and emitted as `Float.valueOf("")`. `Float(double)` is specified as `(float) value`, so it rounds binary64 to binary32, while `Float.valueOf(String)` rounds the decimal straight to binary32, and the two can differ in the last bit: `(float) 1.0000000596046448` is `1.0f` (bits 0x3f800000) while `Float.valueOf("1.0000000596046448")` is one ulp higher (0x3f800001). Drop the String path so a literal argument goes through `Float.valueOf((float) )` like every other primitive double argument. That cast template placed the argument directly in the cast operand. A cast binds tighter than binary, ternary and assignment operators, so a compound argument was only partly covered and the output either failed to compile (`Float.valueOf((float) a + b)` passes a `double`) or rounded one step too early (`(float) huge * 0` is `NaN` rather than `0.0f` for `double huge = 1e39`). Parenthesize the argument for those expression kinds; identifiers, field accesses and method invocations keep the form they already had. The existing `doubleToFloat` test expected `Float.valueOf("2.0")` for `new Float(2.0d)` and now expects `Float.valueOf((float) 2.0d)`. --- ...itiveWrapperClassConstructorToValueOf.java | 17 ++- ...eWrapperClassConstructorToValueOfTest.java | 124 +++++++++++++++++- 2 files changed, 133 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java b/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java index 243d75cee..f7562e700 100644 --- a/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java +++ b/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java @@ -91,16 +91,19 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { valueOf = JavaTemplate.builder("Short.valueOf(#{any(short)})"); break; case "java.lang.Float": - if (arg instanceof J.Literal && JavaType.Primitive.Double == ((J.Literal) arg).getType()) { - arg = ((J.Literal) arg).withType(JavaType.Primitive.String); - arg = ((J.Literal) arg).withValueSource("\"" + ((J.Literal) arg).getValue() + "\""); - } - JavaType argType = arg.getType(); if (TypeUtils.isOfClassType(argType, "java.lang.Double")) { valueOf = JavaTemplate.builder("Float.valueOf(#{any(java.lang.Double)}.floatValue())"); - } else if (JavaType.Primitive.Double == arg.getType()) { - valueOf = JavaTemplate.builder("Float.valueOf((float) #{any(double)})"); + } else if (JavaType.Primitive.Double == argType) { + // A cast binds tighter than these operators, so without parentheses it would cover + // only the first operand. Depending on the argument that either fails to compile + // (`Float.valueOf((float) a + b)` passes a `double`; `(float) d = 2.0` is not an + // assignment target) or silently rounds to `float` one step too early + // (`(float) d * 2`). + valueOf = JavaTemplate.builder(arg instanceof J.Binary || arg instanceof J.Ternary || + arg instanceof J.Assignment || arg instanceof J.AssignmentOperation ? + "Float.valueOf((float) (#{any(double)}))" : + "Float.valueOf((float) #{any(double)})"); } else { valueOf = JavaTemplate.builder("Float.valueOf(#{any(float)})"); } diff --git a/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java b/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java index 31e83bb23..8251a9095 100644 --- a/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java @@ -203,7 +203,7 @@ class T { Double d1 = Double.valueOf(1.0); double d2 = 2.0d; void makeFloats() { - Float f = Float.valueOf("2.0"); + Float f = Float.valueOf((float) 2.0d); Float f2 = Float.valueOf(getD().floatValue()); Float f3 = Float.valueOf(d1.floatValue()); Float f4 = Float.valueOf((float) d2); @@ -217,6 +217,128 @@ Double getD() { ); } + @Test + void doubleLiteralToFloatKeepsBinary64Rounding() { + // `new Float(double)` is defined as `(float) value`, so the literal rounds to binary64 first and + // yields bits 0x3f800000; `Float.valueOf("1.0000000596046448")` rounds the decimal straight to + // binary32 and yields 0x3f800001, one ulp higher. + rewriteRun( + //language=java + java( + """ + class T { + Float value = new Float(1.0000000596046448); + } + """, + """ + class T { + Float value = Float.valueOf((float) 1.0000000596046448); + } + """ + ) + ); + } + + @Test + void doubleLiteralToFloatKeepsSourceForm() { + rewriteRun( + //language=java + java( + """ + class T { + Float hex = new Float(0x1.0000002p0); + Float suffixed = new Float(1.0000000596046448D); + Float subnormal = new Float(4.9E-324); + } + """, + """ + class T { + Float hex = Float.valueOf((float) 0x1.0000002p0); + Float suffixed = Float.valueOf((float) 1.0000000596046448D); + Float subnormal = Float.valueOf((float) 4.9E-324); + } + """ + ) + ); + } + + @Test + void doubleExpressionToFloatUsesCast() { + rewriteRun( + //language=java + java( + """ + class T { + Float negativeZero = new Float(-0.0); + Float overflowing = new Float(Double.MAX_VALUE); + Float parenthesized = new Float((1.0000000596046448)); + } + """, + """ + class T { + Float negativeZero = Float.valueOf((float) -0.0); + Float overflowing = Float.valueOf((float) Double.MAX_VALUE); + Float parenthesized = Float.valueOf((float) (1.0000000596046448)); + } + """ + ) + ); + } + + @Test + void compoundDoubleExpressionToFloatIsParenthesized() { + rewriteRun( + //language=java + java( + """ + class T { + double d1 = 1.0; + double d2 = 2.0; + void makeFloats() { + Float sum = new Float(d1 + d2); + Float ternary = new Float(d1 > d2 ? d1 : d2); + Float assigned = new Float(d1 = 2.0); + Float compound = new Float(d1 += 2.0); + } + } + """, + """ + class T { + double d1 = 1.0; + double d2 = 2.0; + void makeFloats() { + Float sum = Float.valueOf((float) (d1 + d2)); + Float ternary = Float.valueOf((float) (d1 > d2 ? d1 : d2)); + Float assigned = Float.valueOf((float) (d1 = 2.0)); + Float compound = Float.valueOf((float) (d1 += 2.0)); + } + } + """ + ) + ); + } + + @Test + void floatLiteralUnchangedByDoubleHandling() { + rewriteRun( + //language=java + java( + """ + class T { + Float f = new Float(1.1f); + Float hex = new Float(0x1.0000002p0f); + } + """, + """ + class T { + Float f = Float.valueOf(1.1f); + Float hex = Float.valueOf(0x1.0000002p0f); + } + """ + ) + ); + } + @Test void withinEnum() { rewriteRun( From 861c090e59a571a2f8a0a1a476eb68a3c446bea4 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 11 Aug 2026 10:34:03 +0200 Subject: [PATCH 2/3] Trim the cast-parenthesization comment --- .../PrimitiveWrapperClassConstructorToValueOf.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java b/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java index f7562e700..1e6a33f7e 100644 --- a/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java +++ b/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java @@ -95,11 +95,9 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { if (TypeUtils.isOfClassType(argType, "java.lang.Double")) { valueOf = JavaTemplate.builder("Float.valueOf(#{any(java.lang.Double)}.floatValue())"); } else if (JavaType.Primitive.Double == argType) { - // A cast binds tighter than these operators, so without parentheses it would cover - // only the first operand. Depending on the argument that either fails to compile - // (`Float.valueOf((float) a + b)` passes a `double`; `(float) d = 2.0` is not an - // assignment target) or silently rounds to `float` one step too early - // (`(float) d * 2`). + // A cast binds tighter than these operators, so without parentheses it would cover only + // the first operand, either failing to compile (`Float.valueOf((float) a + b)` passes + // a `double`) or rounding to `float` one step too early (`(float) d * 2`). valueOf = JavaTemplate.builder(arg instanceof J.Binary || arg instanceof J.Ternary || arg instanceof J.Assignment || arg instanceof J.AssignmentOperation ? "Float.valueOf((float) (#{any(double)}))" : From d101f14ba5ef01251f0aa54196da482bddc86076 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 12 Aug 2026 00:26:19 +0200 Subject: [PATCH 3/3] Trim commentary --- .../PrimitiveWrapperClassConstructorToValueOf.java | 5 ++--- .../PrimitiveWrapperClassConstructorToValueOfTest.java | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java b/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java index 1e6a33f7e..a3c6ae580 100644 --- a/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java +++ b/src/main/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOf.java @@ -95,9 +95,8 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { if (TypeUtils.isOfClassType(argType, "java.lang.Double")) { valueOf = JavaTemplate.builder("Float.valueOf(#{any(java.lang.Double)}.floatValue())"); } else if (JavaType.Primitive.Double == argType) { - // A cast binds tighter than these operators, so without parentheses it would cover only - // the first operand, either failing to compile (`Float.valueOf((float) a + b)` passes - // a `double`) or rounding to `float` one step too early (`(float) d * 2`). + // A cast binds tighter than these operators, so unparenthesized it covers only the + // first operand, either failing to compile or rounding one step too early valueOf = JavaTemplate.builder(arg instanceof J.Binary || arg instanceof J.Ternary || arg instanceof J.Assignment || arg instanceof J.AssignmentOperation ? "Float.valueOf((float) (#{any(double)}))" : diff --git a/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java b/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java index 8251a9095..0ff588475 100644 --- a/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/PrimitiveWrapperClassConstructorToValueOfTest.java @@ -219,9 +219,8 @@ Double getD() { @Test void doubleLiteralToFloatKeepsBinary64Rounding() { - // `new Float(double)` is defined as `(float) value`, so the literal rounds to binary64 first and - // yields bits 0x3f800000; `Float.valueOf("1.0000000596046448")` rounds the decimal straight to - // binary32 and yields 0x3f800001, one ulp higher. + // `new Float(double)` is `(float) value`, rounding through binary64 to 0x3f800000, where + // `Float.valueOf(String)` rounds the decimal straight to binary32 and yields 0x3f800001 rewriteRun( //language=java java(