From 550d08718d5d38da304d9f9189e3ae8fc041f7b7 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 17:44:11 -0700 Subject: [PATCH 01/16] Unbox wrapper-bounded type variables during numeric promotion; fixes #8328 Erase reference operand types before unboxing during binary numeric promotion. This prevents CFG construction from crashing on expressions such as `t + 1` when `T extends Integer`, while preserving support for captured type variables. Extend the existing `TypeVarPrimitives` all-systems test with arithmetic on type variables in both operand positions. The regression fails with `IllegalArgumentException: T` before the fix and passes afterward. Update the changelog. Validation on JDK 25: - `./gradlew :framework:test --tests '*FrameworkTest*TypeVarPrimitives*'` - `./gradlew :framework:FrameworkTest :checker:NullnessTest :dataflow:allDataflowTests` - `git diff --check` Fixes #8328. Assisted-by: Codex (gpt-6) --- .../cfg/builder/CFGTranslationPhaseOne.java | 21 +++++++------------ docs/CHANGELOG.md | 4 ++++ .../tests/all-systems/TypeVarPrimitives.java | 9 ++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 4867358b5fbe..8a906ff059dd 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -1012,7 +1012,12 @@ protected boolean isNumericOrBoxed(TypeMirror type) { /** * Compute the type to which two numeric types must be promoted before performing a binary numeric - * operation on them. The input types must both be numeric and the output type is primitive. + * operation on them. The input types must both be convertible to numeric primitive types, and the + * output type is primitive. + * + *

Javac's unboxing API accepts wrapper classes, such as {@code Integer}, but rejects type + * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code Integer}, + * which the API can unbox to {@code int}. Erasure handles both ordinary and captured type variables. * * @param left the type of the left operand * @param right the type of the right operand @@ -1020,21 +1025,11 @@ protected boolean isNumericOrBoxed(TypeMirror type) { */ protected TypeMirror binaryPromotedType(TypeMirror left, TypeMirror right) { if (!left.getKind().isPrimitive()) { - if (TypesUtils.isCapturedTypeVariable(left)) { - // This doesn't seem legal according to the JLS, but javac accepts it. - left = types.unboxedType(TypesUtils.upperBound(left)); - } else { - left = types.unboxedType(left); - } + left = types.unboxedType(types.erasure(left)); } if (!right.getKind().isPrimitive()) { - if (TypesUtils.isCapturedTypeVariable(right)) { - // This doesn't seem legal according to the JLS, but javac accepts it. - right = types.unboxedType(TypesUtils.upperBound(right)); - } else { - right = types.unboxedType(right); - } + right = types.unboxedType(types.erasure(right)); } TypeKind promotedTypeKind = TypeKindUtils.widenedNumericType(left, right); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1a8ba768e808..a1c1903cb671 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,10 +7,14 @@ ### User-visible changes +Fixed a crash when unboxing wrapper-bounded type variables in numeric operations. + ### Changes for type system implementers ### Closed issues +\#8328. + ## Version 4.3.0 (2026-10-01) ### User-visible changes diff --git a/framework/tests/all-systems/TypeVarPrimitives.java b/framework/tests/all-systems/TypeVarPrimitives.java index 1ad25d008ba8..077348640b0f 100644 --- a/framework/tests/all-systems/TypeVarPrimitives.java +++ b/framework/tests/all-systems/TypeVarPrimitives.java @@ -9,4 +9,13 @@ void method(T tLong) { void methodIntersection(T tLong) { long l = tLong; } + + // Regression tests for https://github.com/typetools/checker-framework/issues/8328. + int add(T t) { + return t + 1; + } + + long subtract(T t) { + return 1 - t; + } } From e85c5906ea29d746569a5a59454e11e78bc2a9fc Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 17:46:20 -0700 Subject: [PATCH 02/16] spotless --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 8a906ff059dd..9336b7834aeb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -1016,8 +1016,9 @@ protected boolean isNumericOrBoxed(TypeMirror type) { * output type is primitive. * *

Javac's unboxing API accepts wrapper classes, such as {@code Integer}, but rejects type - * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code Integer}, - * which the API can unbox to {@code int}. Erasure handles both ordinary and captured type variables. + * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code + * Integer}, which the API can unbox to {@code int}. Erasure handles both ordinary and captured + * type variables. * * @param left the type of the left operand * @param right the type of the right operand From 1f95233cfeea8fc54fddc90d714f03e8ea790060 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 17:47:08 -0700 Subject: [PATCH 03/16] don't update changelog --- docs/CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a1c1903cb671..1a8ba768e808 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,14 +7,10 @@ ### User-visible changes -Fixed a crash when unboxing wrapper-bounded type variables in numeric operations. - ### Changes for type system implementers ### Closed issues -\#8328. - ## Version 4.3.0 (2026-10-01) ### User-visible changes From 4b33d17b1ede1592dd0de03961462b715d1b5160 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 18:12:59 -0700 Subject: [PATCH 04/16] Build unboxing calls for wrapper-bounded type variables Recognize wrapper-bounded type variables in CFG unboxing and resolve their primitive-value methods using the erased wrapper type. Preserve the original operand as the receiver so the CFG models null-pointer exceptions and normal-path non-null refinement instead of invalid widening conversions. Add CFG regression tests for both operand positions and a nullable-unboxing flow test. Assisted-by: Codex (gpt-6) --- .../nullness/TypeVarPrimitivesNullness.java | 7 ++ .../cfg/builder/CFGTranslationPhaseOne.java | 5 +- .../junit/TypeVariableUnboxingCFGTest.java | 97 +++++++++++++++++++ .../javacutil/trees/TreeBuilder.java | 4 +- 4 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java diff --git a/checker/tests/nullness/TypeVarPrimitivesNullness.java b/checker/tests/nullness/TypeVarPrimitivesNullness.java index b7c10ecacae6..ad3d71c3cae6 100644 --- a/checker/tests/nullness/TypeVarPrimitivesNullness.java +++ b/checker/tests/nullness/TypeVarPrimitivesNullness.java @@ -30,4 +30,11 @@ public class TypeVarPrimitivesNullness { // :: error: [unboxing.of.nullable] long l = tLong; } + + void add(T t) { + // :: error: [unboxing.of.nullable] + int result = t + 1; + // The unboxing operation establishes that t is non-null on the normal path. + t.toString(); + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 9336b7834aeb..28906dc4552b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -881,14 +881,15 @@ protected Node box(Node node) { } /** - * If the input node is a boxed type, unbox it, otherwise leave it alone. + * If the input node is a boxed type or a type variable bounded by a boxed type, unbox it; + * otherwise leave it alone. * * @param node in input node * @return a Node representing the unboxed version of the input, which may simply be the input * node */ protected Node unbox(Node node) { - if (TypesUtils.isBoxedPrimitive(node.getType())) { + if (TypesUtils.isBoxedPrimitive(types.erasure(node.getType()))) { MemberSelectTree primValueSelect = treeBuilder.buildPrimValueMethodAccess(node.getTree()); handleArtificialTree(primValueSelect); diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java new file mode 100644 index 000000000000..04c9aabc4f36 --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java @@ -0,0 +1,97 @@ +package org.checkerframework.framework.test.junit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.lang.model.type.TypeKind; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.node.MethodAccessNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; +import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests the CFG unboxing conversions for wrapper-bounded type variables. */ +public class TypeVariableUnboxingCFGTest { + + /** Temporary directory for compiler inputs and outputs. */ + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + /** Creates a TypeVariableUnboxingCFGTest. */ + public TypeVariableUnboxingCFGTest() {} + + @Test + public void unboxLeftOperand() throws IOException { + ControlFlowGraph cfg = generateCFG("add"); + cfg.checkInvariants(); + NumericalAdditionNode addition = + cfg.getAllNodes().stream() + .filter(NumericalAdditionNode.class::isInstance) + .map(NumericalAdditionNode.class::cast) + .findFirst() + .orElseThrow(); + assertUnboxing(addition.getLeftOperand(), "intValue", TypeKind.INT); + } + + @Test + public void unboxRightOperand() throws IOException { + ControlFlowGraph cfg = generateCFG("subtract"); + cfg.checkInvariants(); + NumericalSubtractionNode subtraction = + cfg.getAllNodes().stream() + .filter(NumericalSubtractionNode.class::isInstance) + .map(NumericalSubtractionNode.class::cast) + .findFirst() + .orElseThrow(); + assertUnboxing(subtraction.getRightOperand(), "longValue", TypeKind.LONG); + } + + /** + * Builds a CFG from a temporary copy of the existing numeric-promotion regression input. + * + * @param method the method whose CFG is built + * @return the method's CFG + * @throws IOException if copying the test input fails + */ + private ControlFlowGraph generateCFG(String method) throws IOException { + Path inputFile = temporaryFolder.getRoot().toPath().resolve("TypeVarPrimitives.java"); + Files.copy(Path.of("tests/all-systems/TypeVarPrimitives.java"), inputFile); + return CFGVisualizeLauncher.generateMethodCFG( + inputFile.toString(), method, "TypeVarPrimitives", null); + } + + /** + * Checks that an operand is unboxed by a primitive-value method call with a null-pointer edge. + * + * @param operand the converted operand of a numeric operation + * @param methodName the expected primitive-value method name + * @param primitiveKind the expected primitive result type + */ + private void assertUnboxing(Node operand, String methodName, TypeKind primitiveKind) { + assertTrue( + "Expected an unboxing method invocation: " + operand, + operand instanceof MethodInvocationNode); + MethodAccessNode access = ((MethodInvocationNode) operand).getTarget(); + assertEquals(methodName, access.getMethod().getSimpleName().toString()); + assertEquals(TypeKind.TYPEVAR, access.getReceiver().getType().getKind()); + assertEquals(primitiveKind, operand.getType().getKind()); + Block block = access.getBlock(); + assertTrue("Expected an exception block for unboxing", block instanceof ExceptionBlock); + assertTrue( + ((ExceptionBlock) block) + .getExceptionalSuccessors().entrySet().stream() + .anyMatch( + entry -> + entry.getKey().toString().equals("java.lang.NullPointerException") + && !entry.getValue().isEmpty())); + } +} diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java b/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java index dfd740257389..682f586ff55c 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java @@ -493,11 +493,11 @@ public static Symbol.MethodSymbol getValueOfMethod( * Builds an AST Tree to access the *Value() method of a boxed type such as Short or Float, where * * is the corresponding primitive type (i.e. shortValue or floatValue). * - * @param expr an expression whose type is a boxed type + * @param expr an expression whose type is a boxed type or a type variable bounded by a boxed type * @return a MemberSelectTree that accesses the *Value() method of the expression */ public MemberSelectTree buildPrimValueMethodAccess(Tree expr) { - TypeMirror boxedType = TreeUtils.typeOf(expr); + TypeMirror boxedType = modelTypes.erasure(TreeUtils.typeOf(expr)); TypeElement boxedElement = (TypeElement) ((DeclaredType) boxedType).asElement(); assert TypesUtils.isBoxedPrimitive(boxedType); From b0b824875988a9d556d20878b5dfaaec93595961 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 20:55:13 -0700 Subject: [PATCH 05/16] Avoid static imports in the unboxing CFG test Use qualified JUnit assertions to satisfy the misc CI import check. Assisted-by: Codex (gpt-6) --- .../test/junit/TypeVariableUnboxingCFGTest.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java index 04c9aabc4f36..317db4a06139 100644 --- a/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java +++ b/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java @@ -1,8 +1,5 @@ package org.checkerframework.framework.test.junit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -16,6 +13,7 @@ import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -77,16 +75,16 @@ private ControlFlowGraph generateCFG(String method) throws IOException { * @param primitiveKind the expected primitive result type */ private void assertUnboxing(Node operand, String methodName, TypeKind primitiveKind) { - assertTrue( + Assert.assertTrue( "Expected an unboxing method invocation: " + operand, operand instanceof MethodInvocationNode); MethodAccessNode access = ((MethodInvocationNode) operand).getTarget(); - assertEquals(methodName, access.getMethod().getSimpleName().toString()); - assertEquals(TypeKind.TYPEVAR, access.getReceiver().getType().getKind()); - assertEquals(primitiveKind, operand.getType().getKind()); + Assert.assertEquals(methodName, access.getMethod().getSimpleName().toString()); + Assert.assertEquals(TypeKind.TYPEVAR, access.getReceiver().getType().getKind()); + Assert.assertEquals(primitiveKind, operand.getType().getKind()); Block block = access.getBlock(); - assertTrue("Expected an exception block for unboxing", block instanceof ExceptionBlock); - assertTrue( + Assert.assertTrue("Expected an exception block for unboxing", block instanceof ExceptionBlock); + Assert.assertTrue( ((ExceptionBlock) block) .getExceptionalSuccessors().entrySet().stream() .anyMatch( From 954f4a99add6fd7dfd53d6ece0bedfc005a70537 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 20:56:52 -0700 Subject: [PATCH 06/16] remove test --- .../junit/TypeVariableUnboxingCFGTest.java | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java deleted file mode 100644 index 317db4a06139..000000000000 --- a/framework/src/test/java/org/checkerframework/framework/test/junit/TypeVariableUnboxingCFGTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.checkerframework.framework.test.junit; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import javax.lang.model.type.TypeKind; -import org.checkerframework.dataflow.cfg.ControlFlowGraph; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ExceptionBlock; -import org.checkerframework.dataflow.cfg.node.MethodAccessNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; -import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; -import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests the CFG unboxing conversions for wrapper-bounded type variables. */ -public class TypeVariableUnboxingCFGTest { - - /** Temporary directory for compiler inputs and outputs. */ - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - /** Creates a TypeVariableUnboxingCFGTest. */ - public TypeVariableUnboxingCFGTest() {} - - @Test - public void unboxLeftOperand() throws IOException { - ControlFlowGraph cfg = generateCFG("add"); - cfg.checkInvariants(); - NumericalAdditionNode addition = - cfg.getAllNodes().stream() - .filter(NumericalAdditionNode.class::isInstance) - .map(NumericalAdditionNode.class::cast) - .findFirst() - .orElseThrow(); - assertUnboxing(addition.getLeftOperand(), "intValue", TypeKind.INT); - } - - @Test - public void unboxRightOperand() throws IOException { - ControlFlowGraph cfg = generateCFG("subtract"); - cfg.checkInvariants(); - NumericalSubtractionNode subtraction = - cfg.getAllNodes().stream() - .filter(NumericalSubtractionNode.class::isInstance) - .map(NumericalSubtractionNode.class::cast) - .findFirst() - .orElseThrow(); - assertUnboxing(subtraction.getRightOperand(), "longValue", TypeKind.LONG); - } - - /** - * Builds a CFG from a temporary copy of the existing numeric-promotion regression input. - * - * @param method the method whose CFG is built - * @return the method's CFG - * @throws IOException if copying the test input fails - */ - private ControlFlowGraph generateCFG(String method) throws IOException { - Path inputFile = temporaryFolder.getRoot().toPath().resolve("TypeVarPrimitives.java"); - Files.copy(Path.of("tests/all-systems/TypeVarPrimitives.java"), inputFile); - return CFGVisualizeLauncher.generateMethodCFG( - inputFile.toString(), method, "TypeVarPrimitives", null); - } - - /** - * Checks that an operand is unboxed by a primitive-value method call with a null-pointer edge. - * - * @param operand the converted operand of a numeric operation - * @param methodName the expected primitive-value method name - * @param primitiveKind the expected primitive result type - */ - private void assertUnboxing(Node operand, String methodName, TypeKind primitiveKind) { - Assert.assertTrue( - "Expected an unboxing method invocation: " + operand, - operand instanceof MethodInvocationNode); - MethodAccessNode access = ((MethodInvocationNode) operand).getTarget(); - Assert.assertEquals(methodName, access.getMethod().getSimpleName().toString()); - Assert.assertEquals(TypeKind.TYPEVAR, access.getReceiver().getType().getKind()); - Assert.assertEquals(primitiveKind, operand.getType().getKind()); - Block block = access.getBlock(); - Assert.assertTrue("Expected an exception block for unboxing", block instanceof ExceptionBlock); - Assert.assertTrue( - ((ExceptionBlock) block) - .getExceptionalSuccessors().entrySet().stream() - .anyMatch( - entry -> - entry.getKey().toString().equals("java.lang.NullPointerException") - && !entry.getValue().isEmpty())); - } -} From 9e622621c7022c721573cb3e7564d4231a0a66b5 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 7 Oct 2026 21:06:50 -0700 Subject: [PATCH 07/16] improve comment --- checker/tests/nullness/TypeVarPrimitivesNullness.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/tests/nullness/TypeVarPrimitivesNullness.java b/checker/tests/nullness/TypeVarPrimitivesNullness.java index ad3d71c3cae6..8795501eed43 100644 --- a/checker/tests/nullness/TypeVarPrimitivesNullness.java +++ b/checker/tests/nullness/TypeVarPrimitivesNullness.java @@ -34,7 +34,8 @@ public class TypeVarPrimitivesNullness { void add(T t) { // :: error: [unboxing.of.nullable] int result = t + 1; - // The unboxing operation establishes that t is non-null on the normal path. + // Make sure CFG construction was sufficient to capture that t must be non-null at this point, + // since the unboxing must have succeeded t.toString(); } } From 03c1f42712d301643eaeefab2c7532ccb5544261 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 8 Oct 2026 07:40:02 -0700 Subject: [PATCH 08/16] Unbox wrapper-bounded type variables in more CFG contexts Handle type variables and intersection types whose erasure is a boxed primitive in assignment, method-invocation, equality, bitwise, relational, and conditional-expression conversions. Do not unbox a type-variable switch selector, since such a switch is a pattern switch. --- .../nullness/TypeVarPrimitivesNullness.java | 26 +++++ checker/tests/nullness/Unboxing.java | 2 +- .../java21/TypeVarSwitchSelector.java | 21 ++++ .../cfg/builder/CFGTranslationPhaseOne.java | 95 ++++++++++++------- .../tests/all-systems/TypeVarPrimitives.java | 29 ++++++ 5 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 checker/tests/nullness/java21/TypeVarSwitchSelector.java diff --git a/checker/tests/nullness/TypeVarPrimitivesNullness.java b/checker/tests/nullness/TypeVarPrimitivesNullness.java index 8795501eed43..bba4d842f6ec 100644 --- a/checker/tests/nullness/TypeVarPrimitivesNullness.java +++ b/checker/tests/nullness/TypeVarPrimitivesNullness.java @@ -38,4 +38,30 @@ public class TypeVarPrimitivesNullness { // since the unboxing must have succeeded t.toString(); } + + void assign(T t) { + // :: error: [unboxing.of.nullable] + int result = t; + t.toString(); + } + + void takeLong(long l) {} + + void invoke(T t) { + // :: error: [unboxing.of.nullable] + takeLong(t); + t.toString(); + } + + void equality(T t) { + // :: error: [unboxing.of.nullable] + boolean result = t == 1; + t.toString(); + } + + void bitwise(T t) { + // :: error: [unboxing.of.nullable] + long result = t & 1L; + t.toString(); + } } diff --git a/checker/tests/nullness/Unboxing.java b/checker/tests/nullness/Unboxing.java index baa143724613..0deb75743295 100644 --- a/checker/tests/nullness/Unboxing.java +++ b/checker/tests/nullness/Unboxing.java @@ -37,7 +37,7 @@ int bar(@Nullable Integer in) { int barT(T in) { // :: error: [unboxing.of.nullable] int q = in; - // :: error: [unboxing.of.nullable] + // no error, since in has been unboxed return in; } } diff --git a/checker/tests/nullness/java21/TypeVarSwitchSelector.java b/checker/tests/nullness/java21/TypeVarSwitchSelector.java new file mode 100644 index 000000000000..470f97801ba9 --- /dev/null +++ b/checker/tests/nullness/java21/TypeVarSwitchSelector.java @@ -0,0 +1,21 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java21-jdk-skip-test + +// None of the WPI formats supports the new Java 21 languages features, so skip inference until they +// do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test +public class TypeVarSwitchSelector { + + // A switch over a selector whose type is a type variable is a pattern switch, so the selector is + // not unboxed and the `case null` branch is reachable. + String patternSwitch(T t) { + return switch (t) { + // :: error: [dereference.of.nullable] + case null -> t.toString(); + case Integer i -> i.toString(); + }; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 28906dc4552b..b4ecad66d254 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -889,7 +889,7 @@ protected Node box(Node node) { * node */ protected Node unbox(Node node) { - if (TypesUtils.isBoxedPrimitive(types.erasure(node.getType()))) { + if (isUnboxable(node.getType())) { MemberSelectTree primValueSelect = treeBuilder.buildPrimValueMethodAccess(node.getTree()); handleArtificialTree(primValueSelect); @@ -915,12 +915,49 @@ protected Node unbox(Node node) { } } + /** + * Returns true if unboxing conversion (JLS 5.1.8) applies to the given type: it is a boxed + * primitive, or a type variable or intersection type whose erasure is a boxed primitive. + * + *

Javac's unboxing API accepts wrapper classes, such as {@code Integer}, but rejects type + * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code + * Integer}, which the API can unbox to {@code int}. Erasure handles both ordinary and captured + * type variables. + * + * @param type a type + * @return true if unboxing conversion applies to the given type + */ + protected boolean isUnboxable(TypeMirror type) { + return TypesUtils.isBoxedPrimitive(types.erasure(type)); + } + + /** + * Returns the primitive type that unboxing conversion produces for the given type. + * + * @param type a type for which {@link #isUnboxable} returns true + * @return the primitive type that unboxing conversion produces for the given type + */ + protected TypeMirror unboxedTypeOf(TypeMirror type) { + return types.unboxedType(types.erasure(type)); + } + + /** + * If unboxing conversion applies to the given type, returns the resulting primitive type; + * otherwise returns the given type. + * + * @param type a type + * @return the unboxed version of the given type, or the given type itself + */ + protected TypeMirror unboxedTypeIfUnboxable(TypeMirror type) { + return isUnboxable(type) ? unboxedTypeOf(type) : type; + } + private TreeInfo getTreeInfo(Tree tree) { TypeMirror type = TreeUtils.typeOf(tree); - boolean boxed = TypesUtils.isBoxedPrimitive(type); - TypeMirror unboxedType = boxed ? types.unboxedType(type) : type; + boolean boxed = isUnboxable(type); + TypeMirror unboxedType = boxed ? unboxedTypeOf(type) : type; - boolean bool = TypesUtils.isBooleanType(type); + boolean bool = TypesUtils.isBooleanType(unboxedType); boolean numeric = TypesUtils.isNumeric(unboxedType); return new TreeInfo() { @@ -1001,14 +1038,14 @@ protected Node unaryNumericPromotion(Node node) { } /** - * Returns true if the argument type is a numeric primitive or a boxed numeric primitive and false - * otherwise. + * Returns true if the argument type is a numeric primitive, or if unboxing conversion converts it + * to a numeric primitive. + * + * @param type a type + * @return true if the argument type is a numeric primitive or can be unboxed to one */ protected boolean isNumericOrBoxed(TypeMirror type) { - if (TypesUtils.isBoxedPrimitive(type)) { - type = types.unboxedType(type); - } - return TypesUtils.isNumeric(type); + return TypesUtils.isNumeric(unboxedTypeIfUnboxable(type)); } /** @@ -1016,22 +1053,17 @@ protected boolean isNumericOrBoxed(TypeMirror type) { * operation on them. The input types must both be convertible to numeric primitive types, and the * output type is primitive. * - *

Javac's unboxing API accepts wrapper classes, such as {@code Integer}, but rejects type - * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code - * Integer}, which the API can unbox to {@code int}. Erasure handles both ordinary and captured - * type variables. - * * @param left the type of the left operand * @param right the type of the right operand * @return a TypeMirror representing the binary numeric promoted type */ protected TypeMirror binaryPromotedType(TypeMirror left, TypeMirror right) { if (!left.getKind().isPrimitive()) { - left = types.unboxedType(types.erasure(left)); + left = unboxedTypeOf(left); } if (!right.getKind().isPrimitive()) { - right = types.unboxedType(types.erasure(right)); + right = unboxedTypeOf(right); } TypeKind promotedTypeKind = TypeKindUtils.widenedNumericType(left, right); @@ -1162,7 +1194,7 @@ protected Node commonConvert(Node node, TypeMirror varType, boolean contextAllow boolean isRightNumeric = TypesUtils.isNumeric(nodeType); boolean isRightPrimitive = TypesUtils.isPrimitive(nodeType); - boolean isRightBoxed = TypesUtils.isBoxedPrimitive(nodeType); + boolean isRightBoxed = isUnboxable(nodeType); boolean isRightReference = nodeType instanceof ReferenceType; boolean isLeftNumeric = TypesUtils.isNumeric(varType); boolean isLeftPrimitive = TypesUtils.isPrimitive(varType); @@ -1398,8 +1430,8 @@ protected Node conditionalExprPromotion(Node node, TypeMirror destType) { // If the operand is byte or Byte and the whole expression is // short, then convert to short. - boolean isBoxedPrimitive = TypesUtils.isBoxedPrimitive(nodeType); - TypeMirror unboxedNodeType = isBoxedPrimitive ? types.unboxedType(nodeType) : nodeType; + boolean isBoxedPrimitive = isUnboxable(nodeType); + TypeMirror unboxedNodeType = isBoxedPrimitive ? unboxedTypeOf(nodeType) : nodeType; TypeMirror unboxedDestType = TypesUtils.isBoxedPrimitive(destType) ? types.unboxedType(destType) : destType; if (TypesUtils.isNumeric(unboxedNodeType) && TypesUtils.isNumeric(unboxedDestType)) { @@ -2221,7 +2253,12 @@ private void buildSelector() { selectorVarUseNode.setInSource(false); extendWithNode(selectorVarUseNode); - Node selectorExprNode = unbox(scan(selectorExprTree, null)); + Node selectorExprNode = scan(selectorExprTree, null); + // A switch whose selector has a type-variable type, such as `T extends Integer`, cannot have + // constant labels, so it is a pattern switch and its selector is not unboxed. + if (TypesUtils.isBoxedPrimitive(selectorExprType)) { + selectorExprNode = unbox(selectorExprNode); + } AssignmentTree assign = treeBuilder.buildAssignment(selectorVarUseTree, selectorExprTree); handleArtificialTree(assign); @@ -3974,17 +4011,8 @@ public Node visitBinary(BinaryTree tree, Void p) { case GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL -> { // see JLS 15.20.1 - TypeMirror leftType = TreeUtils.typeOf(leftTree); - if (TypesUtils.isBoxedPrimitive(leftType)) { - leftType = types.unboxedType(leftType); - } - - TypeMirror rightType = TreeUtils.typeOf(rightTree); - if (TypesUtils.isBoxedPrimitive(rightType)) { - rightType = types.unboxedType(rightType); - } - - TypeMirror promotedType = binaryPromotedType(leftType, rightType); + TypeMirror promotedType = + binaryPromotedType(TreeUtils.typeOf(leftTree), TreeUtils.typeOf(rightTree)); Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); @@ -4036,7 +4064,8 @@ public Node visitBinary(BinaryTree tree, Void p) { TypeMirror leftType = TreeUtils.typeOf(leftTree); TypeMirror rightType = TreeUtils.typeOf(rightTree); boolean isBooleanOp = - TypesUtils.isBooleanType(leftType) && TypesUtils.isBooleanType(rightType); + TypesUtils.isBooleanType(unboxedTypeIfUnboxable(leftType)) + && TypesUtils.isBooleanType(unboxedTypeIfUnboxable(rightType)); Node left; Node right; diff --git a/framework/tests/all-systems/TypeVarPrimitives.java b/framework/tests/all-systems/TypeVarPrimitives.java index 077348640b0f..e66d2730f444 100644 --- a/framework/tests/all-systems/TypeVarPrimitives.java +++ b/framework/tests/all-systems/TypeVarPrimitives.java @@ -18,4 +18,33 @@ int add(T t) { long subtract(T t) { return 1 - t; } + + int assign(T t) { + int result = t; + return result; + } + + boolean equality(T t) { + return t == 1; + } + + int conditional(boolean b, T t) { + return b ? t : 0; + } + + long bitwise(T t) { + return t & 1L; + } + + boolean lessThan(T t) { + return t < 1; + } + + boolean booleanOps(T b) { + return (b & true) || b == false; + } + + long captured(java.util.List l) { + return l.get(0) * 2L; + } } From b98a79d5094633b938fb7c6823b3bec97e0d5c90 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 8 Oct 2026 09:21:59 -0700 Subject: [PATCH 09/16] Add tests for compound boolean assignment and array dimension with type-variable operands --- framework/tests/all-systems/TypeVarArrayDimension.java | 7 +++++++ framework/tests/all-systems/TypeVarPrimitives.java | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 framework/tests/all-systems/TypeVarArrayDimension.java diff --git a/framework/tests/all-systems/TypeVarArrayDimension.java b/framework/tests/all-systems/TypeVarArrayDimension.java new file mode 100644 index 000000000000..20b5013891db --- /dev/null +++ b/framework/tests/all-systems/TypeVarArrayDimension.java @@ -0,0 +1,7 @@ +// Regression test for an array dimension whose type is a type variable with a boxed-primitive +// bound. It is in its own file so that a crash elsewhere does not mask a crash here. +public class TypeVarArrayDimension { + int[] arrayDimension(T t) { + return new int[t]; + } +} diff --git a/framework/tests/all-systems/TypeVarPrimitives.java b/framework/tests/all-systems/TypeVarPrimitives.java index e66d2730f444..a621b2bc8a23 100644 --- a/framework/tests/all-systems/TypeVarPrimitives.java +++ b/framework/tests/all-systems/TypeVarPrimitives.java @@ -44,6 +44,14 @@ boolean booleanOps(T b) { return (b & true) || b == false; } + boolean compoundBoolean(T t) { + boolean x = true; + x &= t; + x |= t; + x ^= t; + return x; + } + long captured(java.util.List l) { return l.get(0) * 2L; } From f4e00792d82f8834bf109eb673b1f1c874982c1e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 8 Oct 2026 09:32:17 -0700 Subject: [PATCH 10/16] Move unboxing helpers to TypesUtils and use them at every unboxing site Erase only type variables and intersection types, and erase at most once per query. --- .../cfg/builder/CFGTranslationPhaseOne.java | 101 +++++------------- .../dataflow/cfg/builder/TreeInfo.java | 7 +- .../javacutil/TypesUtils.java | 51 ++++++++- .../javacutil/trees/TreeBuilder.java | 7 +- 4 files changed, 86 insertions(+), 80 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index b4ecad66d254..aee750afc010 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -889,7 +889,7 @@ protected Node box(Node node) { * node */ protected Node unbox(Node node) { - if (isUnboxable(node.getType())) { + if (TypesUtils.isUnboxable(node.getType(), types)) { MemberSelectTree primValueSelect = treeBuilder.buildPrimValueMethodAccess(node.getTree()); handleArtificialTree(primValueSelect); @@ -915,47 +915,11 @@ protected Node unbox(Node node) { } } - /** - * Returns true if unboxing conversion (JLS 5.1.8) applies to the given type: it is a boxed - * primitive, or a type variable or intersection type whose erasure is a boxed primitive. - * - *

Javac's unboxing API accepts wrapper classes, such as {@code Integer}, but rejects type - * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code - * Integer}, which the API can unbox to {@code int}. Erasure handles both ordinary and captured - * type variables. - * - * @param type a type - * @return true if unboxing conversion applies to the given type - */ - protected boolean isUnboxable(TypeMirror type) { - return TypesUtils.isBoxedPrimitive(types.erasure(type)); - } - - /** - * Returns the primitive type that unboxing conversion produces for the given type. - * - * @param type a type for which {@link #isUnboxable} returns true - * @return the primitive type that unboxing conversion produces for the given type - */ - protected TypeMirror unboxedTypeOf(TypeMirror type) { - return types.unboxedType(types.erasure(type)); - } - - /** - * If unboxing conversion applies to the given type, returns the resulting primitive type; - * otherwise returns the given type. - * - * @param type a type - * @return the unboxed version of the given type, or the given type itself - */ - protected TypeMirror unboxedTypeIfUnboxable(TypeMirror type) { - return isUnboxable(type) ? unboxedTypeOf(type) : type; - } - private TreeInfo getTreeInfo(Tree tree) { TypeMirror type = TreeUtils.typeOf(tree); - boolean boxed = isUnboxable(type); - TypeMirror unboxedType = boxed ? unboxedTypeOf(type) : type; + TypeMirror boxedType = TypesUtils.boxedPrimitiveForUnboxing(type, types); + boolean unboxable = boxedType != null; + TypeMirror unboxedType = boxedType == null ? type : types.unboxedType(boxedType); boolean bool = TypesUtils.isBooleanType(unboxedType); boolean numeric = TypesUtils.isNumeric(unboxedType); @@ -967,8 +931,8 @@ public boolean isNumeric() { } @Override - public boolean isBoxed() { - return boxed; + public boolean isUnboxable() { + return unboxable; } @Override @@ -1045,7 +1009,7 @@ protected Node unaryNumericPromotion(Node node) { * @return true if the argument type is a numeric primitive or can be unboxed to one */ protected boolean isNumericOrBoxed(TypeMirror type) { - return TypesUtils.isNumeric(unboxedTypeIfUnboxable(type)); + return TypesUtils.isNumeric(TypesUtils.unboxedTypeOrSelf(type, types)); } /** @@ -1058,13 +1022,8 @@ protected boolean isNumericOrBoxed(TypeMirror type) { * @return a TypeMirror representing the binary numeric promoted type */ protected TypeMirror binaryPromotedType(TypeMirror left, TypeMirror right) { - if (!left.getKind().isPrimitive()) { - left = unboxedTypeOf(left); - } - - if (!right.getKind().isPrimitive()) { - right = unboxedTypeOf(right); - } + left = TypesUtils.unboxedTypeOrSelf(left, types); + right = TypesUtils.unboxedTypeOrSelf(right, types); TypeKind promotedTypeKind = TypeKindUtils.widenedNumericType(left, right); return types.getPrimitiveType(promotedTypeKind); @@ -1142,8 +1101,9 @@ protected Node narrow(Node node, TypeMirror destType) { * @return a Node with the value narrowed and boxed to the destType, which may be the input node */ protected Node narrowAndBox(Node node, TypeMirror destType) { - if (TypesUtils.isBoxedPrimitive(destType)) { - return box(narrow(node, types.unboxedType(destType))); + TypeMirror boxedDestType = TypesUtils.boxedPrimitiveForUnboxing(destType, types); + if (boxedDestType != null) { + return box(narrow(node, types.unboxedType(boxedDestType))); } else { return narrow(node, destType); } @@ -1159,13 +1119,7 @@ protected Node narrowAndBox(Node node, TypeMirror destType) { protected boolean conversionRequiresNarrowing(TypeMirror varType, Node node) { // Narrowing is restricted to cases where the left-hand side is byte, char, short or Byte, // Char, Short and the right-hand side is a constant. - TypeMirror unboxedVarType = - TypesUtils.isBoxedPrimitive(varType) ? types.unboxedType(varType) : varType; - TypeKind unboxedVarKind = unboxedVarType.getKind(); - boolean isLeftNarrowableTo = - unboxedVarKind == TypeKind.BYTE - || unboxedVarKind == TypeKind.SHORT - || unboxedVarKind == TypeKind.CHAR; + boolean isLeftNarrowableTo = TypesUtils.canBeNarrowingPrimitiveConversion(varType, types); boolean isRightConstant = node instanceof ValueLiteralNode; return isLeftNarrowableTo && isRightConstant; } @@ -1194,7 +1148,7 @@ protected Node commonConvert(Node node, TypeMirror varType, boolean contextAllow boolean isRightNumeric = TypesUtils.isNumeric(nodeType); boolean isRightPrimitive = TypesUtils.isPrimitive(nodeType); - boolean isRightBoxed = isUnboxable(nodeType); + boolean isRightUnboxable = TypesUtils.isUnboxable(nodeType, types); boolean isRightReference = nodeType instanceof ReferenceType; boolean isLeftNumeric = TypesUtils.isNumeric(varType); boolean isLeftPrimitive = TypesUtils.isPrimitive(varType); @@ -1213,7 +1167,7 @@ protected Node commonConvert(Node node, TypeMirror varType, boolean contextAllow } else { node = box(node); } - } else if (isRightBoxed && isLeftPrimitive) { + } else if (isRightUnboxable && isLeftPrimitive) { node = unbox(node); nodeType = node.getType(); @@ -1430,13 +1384,14 @@ protected Node conditionalExprPromotion(Node node, TypeMirror destType) { // If the operand is byte or Byte and the whole expression is // short, then convert to short. - boolean isBoxedPrimitive = isUnboxable(nodeType); - TypeMirror unboxedNodeType = isBoxedPrimitive ? unboxedTypeOf(nodeType) : nodeType; - TypeMirror unboxedDestType = - TypesUtils.isBoxedPrimitive(destType) ? types.unboxedType(destType) : destType; + TypeMirror boxedNodeType = TypesUtils.boxedPrimitiveForUnboxing(nodeType, types); + boolean isNodeUnboxable = boxedNodeType != null; + TypeMirror unboxedNodeType = + boxedNodeType == null ? nodeType : types.unboxedType(boxedNodeType); + TypeMirror unboxedDestType = TypesUtils.unboxedTypeOrSelf(destType, types); if (TypesUtils.isNumeric(unboxedNodeType) && TypesUtils.isNumeric(unboxedDestType)) { if (unboxedNodeType.getKind() == TypeKind.BYTE && destType.getKind() == TypeKind.SHORT) { - if (isBoxedPrimitive) { + if (isNodeUnboxable) { node = unbox(node); } return widen(node, destType); @@ -1446,7 +1401,7 @@ protected Node conditionalExprPromotion(Node node, TypeMirror destType) { // is the unboxed version of it, then apply unboxing. TypeKind destKind = destType.getKind(); if (destKind == TypeKind.BYTE || destKind == TypeKind.CHAR || destKind == TypeKind.SHORT) { - if (isBoxedPrimitive) { + if (isNodeUnboxable) { return unbox(node); } else if (nodeType.getKind() == TypeKind.INT) { return narrow(node, destType); @@ -4037,7 +3992,7 @@ public Node visitBinary(BinaryTree tree, Void p) { if (leftInfo.isNumeric() && rightInfo.isNumeric() - && !(leftInfo.isBoxed() && rightInfo.isBoxed())) { + && !(leftInfo.isUnboxable() && rightInfo.isUnboxable())) { // JLS 15.21.1 numerical equality TypeMirror promotedType = binaryPromotedType(leftInfo.unboxedType(), rightInfo.unboxedType()); @@ -4045,10 +4000,10 @@ public Node visitBinary(BinaryTree tree, Void p) { right = binaryNumericPromotion(right, promotedType); } else if (leftInfo.isBoolean() && rightInfo.isBoolean() - && !(leftInfo.isBoxed() && rightInfo.isBoxed())) { + && !(leftInfo.isUnboxable() && rightInfo.isUnboxable())) { // JLS 15.21.2 boolean equality - left = unboxAsNeeded(left, leftInfo.isBoxed()); - right = unboxAsNeeded(right, rightInfo.isBoxed()); + left = unboxAsNeeded(left, leftInfo.isUnboxable()); + right = unboxAsNeeded(right, rightInfo.isUnboxable()); } if (kind == Tree.Kind.EQUAL_TO) { @@ -4064,8 +4019,8 @@ public Node visitBinary(BinaryTree tree, Void p) { TypeMirror leftType = TreeUtils.typeOf(leftTree); TypeMirror rightType = TreeUtils.typeOf(rightTree); boolean isBooleanOp = - TypesUtils.isBooleanType(unboxedTypeIfUnboxable(leftType)) - && TypesUtils.isBooleanType(unboxedTypeIfUnboxable(rightType)); + TypesUtils.isBooleanType(TypesUtils.unboxedTypeOrSelf(leftType, types)) + && TypesUtils.isBooleanType(TypesUtils.unboxedTypeOrSelf(rightType, types)); Node left; Node right; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java index 32953cdb80fc..6146156c4632 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java @@ -5,11 +5,12 @@ /** A tuple with 4 named elements. */ /*package-private*/ interface TreeInfo { /** - * Returns true if this is boxed. + * Returns true if unboxing conversion applies to this: it is a boxed primitive, or a type + * variable or intersection type whose erasure is a boxed primitive. * - * @return true if this is boxed + * @return true if unboxing conversion applies to this */ - boolean isBoxed(); + boolean isUnboxable(); /** * Returns true if this is numeric. diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java index 4b3bcb1e494c..07d3f2445908 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java @@ -421,6 +421,54 @@ public static boolean isBoxedPrimitive(TypeMirror type) { return isDeclaredOfName(type, fqBoxedTypes); } + /** + * Returns the boxed primitive type that unboxing conversion (JLS 5.1.8) unboxes when applied to + * the given type, or null if unboxing conversion does not apply to the given type. The result is + * the given type itself if it is a boxed primitive, or the erasure of the given type if it is a + * type variable or intersection type whose erasure is a boxed primitive. + * + *

Javac's unboxing API accepts wrapper classes, such as {@code Integer}, but rejects type + * variables. For example, erasing {@code T} when {@code T extends Integer} produces {@code + * Integer}, which the API can unbox to {@code int}. Erasure handles both ordinary and captured + * type variables. + * + * @param type a type + * @param types the type utilities + * @return the boxed primitive type that unboxing conversion unboxes, or null + */ + public static @Nullable TypeMirror boxedPrimitiveForUnboxing(TypeMirror type, Types types) { + TypeKind kind = type.getKind(); + if (kind == TypeKind.TYPEVAR || kind == TypeKind.INTERSECTION) { + type = types.erasure(type); + } + return isBoxedPrimitive(type) ? type : null; + } + + /** + * Returns true if unboxing conversion (JLS 5.1.8) applies to the given type: it is a boxed + * primitive, or a type variable or intersection type whose erasure is a boxed primitive. + * + * @param type a type + * @param types the type utilities + * @return true if unboxing conversion applies to the given type + */ + public static boolean isUnboxable(TypeMirror type, Types types) { + return boxedPrimitiveForUnboxing(type, types) != null; + } + + /** + * If unboxing conversion (JLS 5.1.8) applies to the given type, returns the resulting primitive + * type; otherwise returns the given type. + * + * @param type a type + * @param types the type utilities + * @return the unboxed version of the given type, or the given type itself + */ + public static TypeMirror unboxedTypeOrSelf(TypeMirror type, Types types) { + TypeMirror boxed = boxedPrimitiveForUnboxing(type, types); + return boxed == null ? type : types.unboxedType(boxed); + } + /** * Returns true if this is an immutable type in the JDK. * @@ -1677,8 +1725,7 @@ public static TypeMirror createWildcard( */ public static boolean canBeNarrowingPrimitiveConversion(TypeMirror type, Types types) { // See CFGBuilder.CFGTranslationPhaseOne#conversionRequiresNarrowing() - TypeMirror unboxedType = isBoxedPrimitive(type) ? types.unboxedType(type) : type; - TypeKind unboxedKind = unboxedType.getKind(); + TypeKind unboxedKind = unboxedTypeOrSelf(type, types).getKind(); return unboxedKind == TypeKind.BYTE || unboxedKind == TypeKind.SHORT || unboxedKind == TypeKind.CHAR; diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java b/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java index 682f586ff55c..e5c76bd9070a 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java @@ -497,10 +497,13 @@ public static Symbol.MethodSymbol getValueOfMethod( * @return a MemberSelectTree that accesses the *Value() method of the expression */ public MemberSelectTree buildPrimValueMethodAccess(Tree expr) { - TypeMirror boxedType = modelTypes.erasure(TreeUtils.typeOf(expr)); + TypeMirror exprType = TreeUtils.typeOf(expr); + TypeMirror boxedType = TypesUtils.boxedPrimitiveForUnboxing(exprType, modelTypes); + if (boxedType == null) { + throw new BugInCF("Cannot unbox %s of type %s", expr, exprType); + } TypeElement boxedElement = (TypeElement) ((DeclaredType) boxedType).asElement(); - assert TypesUtils.isBoxedPrimitive(boxedType); TypeMirror unboxedType = modelTypes.unboxedType(boxedType); // Find the *Value() method of the boxed type From 7a1bb8ab8a818a76f36e198b78d0d4948bc30a75 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 12 Oct 2026 12:51:40 -0700 Subject: [PATCH 11/16] fixes --- .../checker/units/UnitsVisitor.java | 6 +++++- .../cfg/builder/CFGTranslationPhaseOne.java | 12 +++++++++++- .../common/value/ValueTreeAnnotator.java | 8 +++++--- .../all-systems/TypeVarArrayDimension.java | 1 + framework/tests/value/TypeVars.java | 17 +++++++++++++++++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java index 7a4feb77a1d2..68022230df51 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java @@ -7,6 +7,7 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.util.AnnotatedTypes; /** * Units visitor. @@ -31,7 +32,10 @@ public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { if (!typeHierarchy.isSubtypeShallowEffective(exprType, varType)) { checker.reportError(tree, "compound.assignment", varType, exprType); } - } else if (!exprType.hasPrimaryAnnotation(UnknownUnits.class)) { + } else if (!atypeFactory.areSameByClass( + AnnotatedTypes.findEffectiveAnnotationInHierarchy( + atypeFactory.getQualifierHierarchy(), exprType, atypeFactory.TOP), + UnknownUnits.class)) { // Only allow mul/div with unqualified units checker.reportError(tree, "compound.assignment", varType, exprType); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index aee750afc010..810641285f65 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -915,6 +915,15 @@ protected Node unbox(Node node) { } } + /** + * Returns information about the tree's type for deciding which operand conversions to apply. + * Records whether unboxing conversion applies and whether the type is numeric or boolean after + * unboxing. The recorded type is the unboxed type if unboxing applies, or the original type + * otherwise. + * + * @param tree the tree whose type to inspect + * @return information about the tree's type and its unboxing conversion + */ private TreeInfo getTreeInfo(Tree tree) { TypeMirror type = TreeUtils.typeOf(tree); TypeMirror boxedType = TypesUtils.boxedPrimitiveForUnboxing(type, types); @@ -3682,7 +3691,8 @@ public Node visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { TypeMirror promotedType = binaryPromotedType(leftType, rightType); targetRHS = binaryNumericPromotion(targetLHS, promotedType); value = binaryNumericPromotion(value, promotedType); - } else if (TypesUtils.isBooleanType(leftType) && TypesUtils.isBooleanType(rightType)) { + } else if (TypesUtils.isBooleanType(TypesUtils.unboxedTypeOrSelf(leftType, types)) + && TypesUtils.isBooleanType(TypesUtils.unboxedTypeOrSelf(rightType, types))) { targetRHS = unbox(targetLHS); value = unbox(value); } else { diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java index 7e20b543b873..611097dc8d56 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java @@ -32,6 +32,7 @@ import org.checkerframework.common.value.util.Range; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -129,9 +130,10 @@ private void handleDimensions( (AnnotatedTypeMirror.AnnotatedArrayType) type.getComponentType()); } AnnotationMirror dimType = - atypeFactory - .getAnnotatedType(dimensions.get(0)) - .getPrimaryAnnotationInHierarchy(atypeFactory.UNKNOWNVAL); + AnnotatedTypes.findEffectiveAnnotationInHierarchy( + atypeFactory.getQualifierHierarchy(), + atypeFactory.getAnnotatedType(dimensions.get(0)), + atypeFactory.UNKNOWNVAL); if (AnnotationUtils.areSameByName(dimType, atypeFactory.BOTTOMVAL)) { type.replaceAnnotation(atypeFactory.BOTTOMVAL); diff --git a/framework/tests/all-systems/TypeVarArrayDimension.java b/framework/tests/all-systems/TypeVarArrayDimension.java index 20b5013891db..0d33528ee441 100644 --- a/framework/tests/all-systems/TypeVarArrayDimension.java +++ b/framework/tests/all-systems/TypeVarArrayDimension.java @@ -1,6 +1,7 @@ // Regression test for an array dimension whose type is a type variable with a boxed-primitive // bound. It is in its own file so that a crash elsewhere does not mask a crash here. public class TypeVarArrayDimension { + @SuppressWarnings("index:array.length.negative") int[] arrayDimension(T t) { return new int[t]; } diff --git a/framework/tests/value/TypeVars.java b/framework/tests/value/TypeVars.java index f9ce7923b4ba..9b37dc09381b 100644 --- a/framework/tests/value/TypeVars.java +++ b/framework/tests/value/TypeVars.java @@ -1,4 +1,21 @@ +import org.checkerframework.common.value.qual.ArrayLen; +import org.checkerframework.common.value.qual.ArrayLenRange; +import org.checkerframework.common.value.qual.IntRange; +import org.checkerframework.common.value.qual.IntVal; + public class TypeVars { + void arrayDimension(T size) { + int @ArrayLen(3) [] array = new int[size]; + // :: error: [assignment] + int @ArrayLen(4) [] wrongLength = new int[size]; + } + + void arrayDimensionRange(U size) { + int @ArrayLenRange(from = 2, to = 5) [] array = new int[size]; + // :: error: [assignment] + int @ArrayLen(3) [] wrongLength = new int[size]; + } + private void test(K key, V value) { String s = "Negative size: " + key + "=" + value; } From 6dc5012f35ecab06ee7e67dffc542ed7336b23f5 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 12 Oct 2026 12:53:00 -0700 Subject: [PATCH 12/16] new test --- .../tests/units/TypeVarCompoundAssignment.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 checker/tests/units/TypeVarCompoundAssignment.java diff --git a/checker/tests/units/TypeVarCompoundAssignment.java b/checker/tests/units/TypeVarCompoundAssignment.java new file mode 100644 index 000000000000..b82165637bcf --- /dev/null +++ b/checker/tests/units/TypeVarCompoundAssignment.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.units.qual.m; + +public class TypeVarCompoundAssignment { + void scalar(T factor, @m int distance) { + distance *= factor; + distance /= factor; + } + + void units(T factor, @m int distance) { + // :: error: [compound.assignment] + distance *= factor; + // :: error: [compound.assignment] + distance /= factor; + } +} From 4ae218293f2df5aea3f6cc2dc14fc985fde3f742 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 12 Oct 2026 13:58:38 -0700 Subject: [PATCH 13/16] fix another value checker crash --- .../common/value/ValueCheckerUtils.java | 8 +++ .../common/value/ValueTreeAnnotator.java | 17 ++++-- framework/tests/value/TypeVarUnboxing.java | 60 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 framework/tests/value/TypeVarUnboxing.java diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java index d1e8bd63f92a..fbd44647122f 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java @@ -47,6 +47,9 @@ public static List getValuesCastedToType( /** * Returns a list of the values of an annotation, and then cast the values to a given type. * + *

A type variable or intersection type that can be unboxed is converted using its boxed + * primitive bound. + * * @param anno the annotation that contains values * @param castTo the unannotated type that is casted to * @param isUnsigned true if the type being casted to is unsigned @@ -58,6 +61,11 @@ public static List getValuesCastedToType( TypeMirror castTo, boolean isUnsigned, ValueAnnotatedTypeFactory atypeFactory) { + // A type variable bounded by a primitive wrapper has that wrapper's runtime representation. + TypeMirror boxedType = TypesUtils.boxedPrimitiveForUnboxing(castTo, atypeFactory.types); + if (boxedType != null) { + castTo = boxedType; + } Class castType = TypesUtils.getClassFromType(castTo); List values; switch (AnnotationUtils.annotationName(anno)) { diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java index 611097dc8d56..13243e60c262 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java @@ -18,6 +18,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; @@ -440,8 +441,8 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror } } - if (!methodIsStaticallyExecutable(TreeUtils.elementFromUse(tree)) - || !handledByValueChecker(type)) { + ExecutableElement method = TreeUtils.elementFromUse(tree); + if (!methodIsStaticallyExecutable(method) || !handledByValueChecker(type)) { return null; } @@ -475,9 +476,17 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror argValues = null; } else { argValues = new ArrayList<>(arguments.size()); - for (ExpressionTree argument : arguments) { + List parameters = method.getParameters(); + for (int i = 0; i < arguments.size(); i++) { + ExpressionTree argument = arguments.get(i); AnnotatedTypeMirror argType = atypeFactory.getAnnotatedType(argument); - List values = getValues(argType, argType.getUnderlyingType()); + TypeMirror argumentType = argType.getUnderlyingType(); + if (i < parameters.size() && parameters.get(i).asType().getKind().isPrimitive()) { + // Synthetic boxing calls can use a tree from before a narrowing conversion, such as + // Byte.valueOf(t + 1). Convert its values to the primitive parameter type for reflection. + argumentType = parameters.get(i).asType(); + } + List values = getValues(argType, argumentType); if (values == null || values.isEmpty()) { // Values aren't known, so don't try to evaluate the method. return null; diff --git a/framework/tests/value/TypeVarUnboxing.java b/framework/tests/value/TypeVarUnboxing.java new file mode 100644 index 000000000000..e739b2d13b72 --- /dev/null +++ b/framework/tests/value/TypeVarUnboxing.java @@ -0,0 +1,60 @@ +import org.checkerframework.common.value.qual.BoolVal; +import org.checkerframework.common.value.qual.DoubleVal; +import org.checkerframework.common.value.qual.IntVal; + +/** Regression test for evaluating unboxing of a type variable with a known value. */ +public class TypeVarUnboxing { + void increment(@IntVal(1) T t) { + // :: error: [unary.increment] + t++; + } + + void byteValues(@IntVal(1) T input) { + @IntVal({1, 2, 3}) T t = input; + @IntVal(1) byte before = t++; + @IntVal(2) byte after = t; + @IntVal(2) byte beforeDecrement = t--; + @IntVal(1) byte afterDecrement = t; + // :: error: [assignment] + @IntVal(2) byte wrong = t; + } + + void shortValue(@IntVal(1) T t) { + @IntVal(2) int result = t + 1; + } + + void characterValue(@IntVal(65) T t) { + @IntVal(65) char result = t; + // :: error: [assignment] + @IntVal(66) char wrong = t; + } + + void integerValue(@IntVal(1) T t) { + @IntVal(2) int result = t + 1; + } + + void longValue(@IntVal(1) T t) { + @IntVal(2) long result = t + 1; + } + + void floatValue(@DoubleVal(1.5) T t) { + @DoubleVal(2.5) float result = t + 1; + } + + void doubleValue(@DoubleVal(1.5) T t) { + @DoubleVal(2.5) double result = t + 1; + } + + void booleanValue(@BoolVal(true) T t) { + @BoolVal(true) boolean result = t; + @BoolVal(false) boolean negated = !t; + } + + void chainedBounds(@IntVal(1) U t) { + @IntVal(1) byte result = t; + } + + void intersectionBound(@IntVal(1) T t) { + @IntVal(1) byte result = t; + } +} From 97d8401fbc53c3183f0055e2477bb20338c08bd6 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Tue, 13 Oct 2026 09:15:54 -0700 Subject: [PATCH 14/16] Revert the Units and Value effective-annotation fixes These changes are now in https://github.com/typetools/checker-framework/pull/8349. --- .../checker/units/UnitsVisitor.java | 6 +----- .../tests/units/TypeVarCompoundAssignment.java | 15 --------------- .../common/value/ValueTreeAnnotator.java | 8 +++----- .../all-systems/TypeVarArrayDimension.java | 8 -------- framework/tests/value/TypeVars.java | 17 ----------------- 5 files changed, 4 insertions(+), 50 deletions(-) delete mode 100644 checker/tests/units/TypeVarCompoundAssignment.java delete mode 100644 framework/tests/all-systems/TypeVarArrayDimension.java diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java index 68022230df51..7a4feb77a1d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java @@ -7,7 +7,6 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.util.AnnotatedTypes; /** * Units visitor. @@ -32,10 +31,7 @@ public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { if (!typeHierarchy.isSubtypeShallowEffective(exprType, varType)) { checker.reportError(tree, "compound.assignment", varType, exprType); } - } else if (!atypeFactory.areSameByClass( - AnnotatedTypes.findEffectiveAnnotationInHierarchy( - atypeFactory.getQualifierHierarchy(), exprType, atypeFactory.TOP), - UnknownUnits.class)) { + } else if (!exprType.hasPrimaryAnnotation(UnknownUnits.class)) { // Only allow mul/div with unqualified units checker.reportError(tree, "compound.assignment", varType, exprType); } diff --git a/checker/tests/units/TypeVarCompoundAssignment.java b/checker/tests/units/TypeVarCompoundAssignment.java deleted file mode 100644 index b82165637bcf..000000000000 --- a/checker/tests/units/TypeVarCompoundAssignment.java +++ /dev/null @@ -1,15 +0,0 @@ -import org.checkerframework.checker.units.qual.m; - -public class TypeVarCompoundAssignment { - void scalar(T factor, @m int distance) { - distance *= factor; - distance /= factor; - } - - void units(T factor, @m int distance) { - // :: error: [compound.assignment] - distance *= factor; - // :: error: [compound.assignment] - distance /= factor; - } -} diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java index 13243e60c262..19d8737294e1 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java @@ -33,7 +33,6 @@ import org.checkerframework.common.value.util.Range; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -131,10 +130,9 @@ private void handleDimensions( (AnnotatedTypeMirror.AnnotatedArrayType) type.getComponentType()); } AnnotationMirror dimType = - AnnotatedTypes.findEffectiveAnnotationInHierarchy( - atypeFactory.getQualifierHierarchy(), - atypeFactory.getAnnotatedType(dimensions.get(0)), - atypeFactory.UNKNOWNVAL); + atypeFactory + .getAnnotatedType(dimensions.get(0)) + .getPrimaryAnnotationInHierarchy(atypeFactory.UNKNOWNVAL); if (AnnotationUtils.areSameByName(dimType, atypeFactory.BOTTOMVAL)) { type.replaceAnnotation(atypeFactory.BOTTOMVAL); diff --git a/framework/tests/all-systems/TypeVarArrayDimension.java b/framework/tests/all-systems/TypeVarArrayDimension.java deleted file mode 100644 index 0d33528ee441..000000000000 --- a/framework/tests/all-systems/TypeVarArrayDimension.java +++ /dev/null @@ -1,8 +0,0 @@ -// Regression test for an array dimension whose type is a type variable with a boxed-primitive -// bound. It is in its own file so that a crash elsewhere does not mask a crash here. -public class TypeVarArrayDimension { - @SuppressWarnings("index:array.length.negative") - int[] arrayDimension(T t) { - return new int[t]; - } -} diff --git a/framework/tests/value/TypeVars.java b/framework/tests/value/TypeVars.java index 9b37dc09381b..f9ce7923b4ba 100644 --- a/framework/tests/value/TypeVars.java +++ b/framework/tests/value/TypeVars.java @@ -1,21 +1,4 @@ -import org.checkerframework.common.value.qual.ArrayLen; -import org.checkerframework.common.value.qual.ArrayLenRange; -import org.checkerframework.common.value.qual.IntRange; -import org.checkerframework.common.value.qual.IntVal; - public class TypeVars { - void arrayDimension(T size) { - int @ArrayLen(3) [] array = new int[size]; - // :: error: [assignment] - int @ArrayLen(4) [] wrongLength = new int[size]; - } - - void arrayDimensionRange(U size) { - int @ArrayLenRange(from = 2, to = 5) [] array = new int[size]; - // :: error: [assignment] - int @ArrayLen(3) [] wrongLength = new int[size]; - } - private void test(K key, V value) { String s = "Negative size: " + key + "=" + value; } From 685297a2dd941cdbcc6c1c60b24f2b0e484fd681 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Tue, 13 Oct 2026 09:16:29 -0700 Subject: [PATCH 15/16] Use TypesUtils.isUnboxable and unboxedTypeOrSelf in CFGTranslationPhaseOne --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 810641285f65..b5b53e7149ba 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -926,9 +926,8 @@ protected Node unbox(Node node) { */ private TreeInfo getTreeInfo(Tree tree) { TypeMirror type = TreeUtils.typeOf(tree); - TypeMirror boxedType = TypesUtils.boxedPrimitiveForUnboxing(type, types); - boolean unboxable = boxedType != null; - TypeMirror unboxedType = boxedType == null ? type : types.unboxedType(boxedType); + boolean unboxable = TypesUtils.isUnboxable(type, types); + TypeMirror unboxedType = TypesUtils.unboxedTypeOrSelf(type, types); boolean bool = TypesUtils.isBooleanType(unboxedType); boolean numeric = TypesUtils.isNumeric(unboxedType); @@ -1393,10 +1392,8 @@ protected Node conditionalExprPromotion(Node node, TypeMirror destType) { // If the operand is byte or Byte and the whole expression is // short, then convert to short. - TypeMirror boxedNodeType = TypesUtils.boxedPrimitiveForUnboxing(nodeType, types); - boolean isNodeUnboxable = boxedNodeType != null; - TypeMirror unboxedNodeType = - boxedNodeType == null ? nodeType : types.unboxedType(boxedNodeType); + boolean isNodeUnboxable = TypesUtils.isUnboxable(nodeType, types); + TypeMirror unboxedNodeType = TypesUtils.unboxedTypeOrSelf(nodeType, types); TypeMirror unboxedDestType = TypesUtils.unboxedTypeOrSelf(destType, types); if (TypesUtils.isNumeric(unboxedNodeType) && TypesUtils.isNumeric(unboxedDestType)) { if (unboxedNodeType.getKind() == TypeKind.BYTE && destType.getKind() == TypeKind.SHORT) { From 4c153a413598dffcdea996d0f81c54b6f6347351 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Tue, 13 Oct 2026 09:38:56 -0700 Subject: [PATCH 16/16] Revert the Value Checker argument-conversion fix This change is now in https://github.com/typetools/checker-framework/pull/8349. --- .../common/value/ValueTreeAnnotator.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java index 19d8737294e1..7e20b543b873 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java @@ -18,7 +18,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; @@ -439,8 +438,8 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror } } - ExecutableElement method = TreeUtils.elementFromUse(tree); - if (!methodIsStaticallyExecutable(method) || !handledByValueChecker(type)) { + if (!methodIsStaticallyExecutable(TreeUtils.elementFromUse(tree)) + || !handledByValueChecker(type)) { return null; } @@ -474,17 +473,9 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror argValues = null; } else { argValues = new ArrayList<>(arguments.size()); - List parameters = method.getParameters(); - for (int i = 0; i < arguments.size(); i++) { - ExpressionTree argument = arguments.get(i); + for (ExpressionTree argument : arguments) { AnnotatedTypeMirror argType = atypeFactory.getAnnotatedType(argument); - TypeMirror argumentType = argType.getUnderlyingType(); - if (i < parameters.size() && parameters.get(i).asType().getKind().isPrimitive()) { - // Synthetic boxing calls can use a tree from before a narrowing conversion, such as - // Byte.valueOf(t + 1). Convert its values to the primitive parameter type for reflection. - argumentType = parameters.get(i).asType(); - } - List values = getValues(argType, argumentType); + List values = getValues(argType, argType.getUnderlyingType()); if (values == null || values.isEmpty()) { // Values aren't known, so don't try to evaluate the method. return null;