Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
550d087
Unbox wrapper-bounded type variables during numeric promotion; fixes …
msridhar Oct 8, 2026
e85c590
spotless
msridhar Oct 8, 2026
1f95233
don't update changelog
msridhar Oct 8, 2026
4b33d17
Build unboxing calls for wrapper-bounded type variables
msridhar Oct 8, 2026
b0b8248
Avoid static imports in the unboxing CFG test
msridhar Oct 8, 2026
954f4a9
remove test
msridhar Oct 8, 2026
9e62262
improve comment
msridhar Oct 8, 2026
03c1f42
Unbox wrapper-bounded type variables in more CFG contexts
mernst Oct 8, 2026
3abf339
Merge pull request #78 from mernst/fix-8328-unboxing-fixes
msridhar Oct 8, 2026
b98a79d
Add tests for compound boolean assignment and array dimension with ty…
mernst Oct 8, 2026
f4e0079
Move unboxing helpers to TypesUtils and use them at every unboxing site
mernst Oct 8, 2026
2111b3b
Merge pull request #79 from mernst/fix-8328-unboxing-crash-tests
msridhar Oct 8, 2026
6bcf3e9
Merge branch 'master' into fix-8328-unboxing
msridhar Oct 9, 2026
3b4da02
Merge branch 'master' into fix-8328-unboxing
msridhar Oct 12, 2026
7a1bb8a
fixes
msridhar Oct 12, 2026
6dc5012
new test
msridhar Oct 12, 2026
ee71fac
Merge branch 'master' into fix-8328-unboxing
msridhar Oct 12, 2026
4ae2182
fix another value checker crash
msridhar Oct 12, 2026
97d8401
Revert the Units and Value effective-annotation fixes
smillst Oct 13, 2026
685297a
Use TypesUtils.isUnboxable and unboxedTypeOrSelf in CFGTranslationPha…
smillst Oct 13, 2026
4c153a4
Revert the Value Checker argument-conversion fix
smillst Oct 13, 2026
7ec455a
Merge branch 'master' into fix-8328-unboxing
smillst Oct 13, 2026
7ff26a4
Merge branch 'master' into fix-8328-unboxing
smillst Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions checker/tests/nullness/TypeVarPrimitivesNullness.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,38 @@ public class TypeVarPrimitivesNullness {
// :: error: [unboxing.of.nullable]
long l = tLong;
}

<T extends @Nullable Integer> void add(T t) {
// :: error: [unboxing.of.nullable]
int result = t + 1;
// 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();
}

<T extends @Nullable Integer> void assign(T t) {
// :: error: [unboxing.of.nullable]
int result = t;
t.toString();
}

void takeLong(long l) {}

<T extends @Nullable Integer> void invoke(T t) {
// :: error: [unboxing.of.nullable]
takeLong(t);
t.toString();
}

<T extends @Nullable Integer> void equality(T t) {
// :: error: [unboxing.of.nullable]
boolean result = t == 1;
t.toString();
}

<T extends @Nullable Integer> void bitwise(T t) {
// :: error: [unboxing.of.nullable]
long result = t & 1L;
t.toString();
}
}
2 changes: 1 addition & 1 deletion checker/tests/nullness/Unboxing.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ int bar(@Nullable Integer in) {
<T extends @Nullable Integer> int barT(T in) {
// :: error: [unboxing.of.nullable]
int q = in;
// :: error: [unboxing.of.nullable]
// no error, since in has been unboxed
return in;
}
}
21 changes: 21 additions & 0 deletions checker/tests/nullness/java21/TypeVarSwitchSelector.java
Original file line number Diff line number Diff line change
@@ -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.
<T extends @Nullable Integer> String patternSwitch(T t) {
return switch (t) {
// :: error: [dereference.of.nullable]
case null -> t.toString();
case Integer i -> i.toString();
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.isUnboxable(node.getType(), types)) {

MemberSelectTree primValueSelect = treeBuilder.buildPrimValueMethodAccess(node.getTree());
handleArtificialTree(primValueSelect);
Expand All @@ -914,12 +915,21 @@ 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);
boolean boxed = TypesUtils.isBoxedPrimitive(type);
TypeMirror unboxedType = boxed ? types.unboxedType(type) : type;
boolean unboxable = TypesUtils.isUnboxable(type, types);
TypeMirror unboxedType = TypesUtils.unboxedTypeOrSelf(type, types);

boolean bool = TypesUtils.isBooleanType(type);
boolean bool = TypesUtils.isBooleanType(unboxedType);
boolean numeric = TypesUtils.isNumeric(unboxedType);

return new TreeInfo() {
Expand All @@ -929,8 +939,8 @@ public boolean isNumeric() {
}

@Override
public boolean isBoxed() {
return boxed;
public boolean isUnboxable() {
return unboxable;
}

@Override
Expand Down Expand Up @@ -1000,42 +1010,28 @@ 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(TypesUtils.unboxedTypeOrSelf(type, types));
}

/**
* 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.
*
* @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()) {
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);
}
}

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);
}
}
left = TypesUtils.unboxedTypeOrSelf(left, types);
right = TypesUtils.unboxedTypeOrSelf(right, types);

TypeKind promotedTypeKind = TypeKindUtils.widenedNumericType(left, right);
return types.getPrimitiveType(promotedTypeKind);
Expand Down Expand Up @@ -1113,8 +1109,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)));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
return narrow(node, destType);
}
Expand All @@ -1130,13 +1127,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;
}
Expand Down Expand Up @@ -1165,7 +1156,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 isRightUnboxable = TypesUtils.isUnboxable(nodeType, types);
boolean isRightReference = nodeType instanceof ReferenceType;
boolean isLeftNumeric = TypesUtils.isNumeric(varType);
boolean isLeftPrimitive = TypesUtils.isPrimitive(varType);
Expand All @@ -1184,7 +1175,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();

Expand Down Expand Up @@ -1401,13 +1392,12 @@ 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;
TypeMirror unboxedDestType =
TypesUtils.isBoxedPrimitive(destType) ? types.unboxedType(destType) : destType;
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) {
if (isBoxedPrimitive) {
if (isNodeUnboxable) {
node = unbox(node);
}
return widen(node, destType);
Expand All @@ -1417,7 +1407,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);
Expand Down Expand Up @@ -2224,7 +2214,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);
Expand Down Expand Up @@ -3693,7 +3688,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 {
Expand Down Expand Up @@ -3977,17 +3973,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);

Expand All @@ -4012,18 +3999,18 @@ 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());
left = binaryNumericPromotion(left, promotedType);
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) {
Expand All @@ -4039,7 +4026,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(TypesUtils.unboxedTypeOrSelf(leftType, types))
&& TypesUtils.isBooleanType(TypesUtils.unboxedTypeOrSelf(rightType, types));

Node left;
Node right;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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
Expand All @@ -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)) {
Expand Down
Loading
Loading