Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
public class NullnessVisitor
extends InitializationVisitor<NullnessAnnotatedTypeFactory, NullnessValue, NullnessStore> {
// Error message keys
// private static final @CompilerMessageKey String ASSIGNMENT_TYPE_INCOMPATIBLE = "assignment";
// private static final @CompilerMessageKey String ASSIGNMENT_TYPE_INCOMPATIBLE
// = "assignment";
private static final @CompilerMessageKey String UNBOXING_OF_NULLABLE = "unboxing.of.nullable";
private static final @CompilerMessageKey String LOCKING_NULLABLE = "locking.nullable";
private static final @CompilerMessageKey String THROWING_NULLABLE = "throwing.nullable";
Expand Down Expand Up @@ -140,7 +141,8 @@ public NullnessAnnotatedTypeFactory createTypeFactory() {

@Override
public boolean isValidUse(AnnotatedPrimitiveType type, Tree tree) {
// The Nullness Checker issues a more comprehensible "nullness.on.primitive" error rather
// The Nullness Checker issues a more comprehensible "nullness.on.primitive"
// error rather
// than the "annotations.on.use" error this method would issue.
return true;
}
Expand All @@ -152,10 +154,13 @@ protected boolean commonAssignmentCheck(
@CompilerMessageKey String errorKey,
Object... extraArgs) {

// Allow a MonotonicNonNull field to be initialized to null at its declaration, in a
// constructor, or in an initializer block. (The latter two are, strictly speaking, unsound
// because the constructor or initializer block might have previously set the field to a
// non-null value. Maybe add an option to disable that behavior.)
// Allow a MonotonicNonNull field to be initialized to null at its declaration,
// in a
// constructor, or in an initializer block. (The latter two are, strictly
// speaking, unsound
// because the constructor or initializer block might have previously set the
// field to a
// non-null value. Maybe add an option to disable that behavior.)
Element elem = initializedElement(varTree);
if (elem != null
&& atypeFactory.fromElement(elem).hasEffectiveAnnotation(MONOTONIC_NONNULL)
Expand Down Expand Up @@ -184,9 +189,10 @@ protected boolean commonAssignmentCheck(
MemberSelectTree mst = (MemberSelectTree) varTree;
ExpressionTree receiver = mst.getExpression();
// This recognizes "this.fieldname = ..." but not "MyClass.fieldname = ..." or
// "MyClass.this.fieldname = ...". The latter forms are probably rare in a
// "MyClass.this.fieldname = ...". The latter forms are probably rare in a
// constructor.
// Note that this method should return non-null only for fields of this class, not
// Note that this method should return non-null only for fields of this class,
// not
// fields of any other class, including outer classes.
if (!(receiver instanceof IdentifierTree)
|| !((IdentifierTree) receiver).getName().contentEquals("this")) {
Expand All @@ -212,9 +218,12 @@ protected boolean commonAssignmentCheck(
ExpressionTree valueExp,
@CompilerMessageKey String errorKey,
Object... extraArgs) {
// Use the valueExp as the context because data flow will have a value for that tree. It
// might not have a value for the var tree. This is sound because if data flow has
// determined @PolyNull is @Nullable at the RHS, then it is also @Nullable for the LHS.
// Use the valueExp as the context because data flow will have a value for that
// tree. It
// might not have a value for the var tree. This is sound because if data flow
// has
// determined @PolyNull is @Nullable at the RHS, then it is also @Nullable for
// the LHS.
atypeFactory.replacePolyQualifier(varType, valueExp);
return super.commonAssignmentCheck(varType, valueExp, errorKey, extraArgs);
}
Expand Down Expand Up @@ -382,9 +391,12 @@ public Void visitAssert(AssertTree tree, Void p) {
// See also
// org.checkerframework.dataflow.cfg.builder.CFGBuilder.CFGTranslationPhaseOne.visitAssert

// In cases where neither assumeAssertionsAreEnabled nor assumeAssertionsAreDisabled are
// turned on and @AssumeAssertions is not used, checkForNullability is still called since
// the CFGBuilder will have generated one branch for which asserts are assumed to be
// In cases where neither assumeAssertionsAreEnabled nor
// assumeAssertionsAreDisabled are
// turned on and @AssumeAssertions is not used, checkForNullability is still
// called since
// the CFGBuilder will have generated one branch for which asserts are assumed
// to be
// enabled.

boolean doVisitAssert;
Expand Down Expand Up @@ -427,7 +439,8 @@ public Void visitInstanceOf(InstanceOfTree tree, Void p) {
}
}
}
// Don't call super because it will issue an incorrect instanceof.unsafe warning.
// Don't call super because it will issue an incorrect instanceof.unsafe
// warning.
// Instead, just scan the part before "instanceof".
super.scan(tree.getExpression(), p);
return null;
Expand Down Expand Up @@ -555,7 +568,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) {
* @param tree a method invocation whose first formal parameter is of String type
* @return the first argument if it is a literal, otherwise null
*/
/*package-private*/ static @Nullable String literalFirstArgument(MethodInvocationTree tree) {
/* package-private */ static @Nullable String literalFirstArgument(MethodInvocationTree tree) {
List<? extends ExpressionTree> args = tree.getArguments();
assert !args.isEmpty();
ExpressionTree arg = args.get(0);
Expand Down Expand Up @@ -683,7 +696,8 @@ private boolean isUnboxingOperation(BinaryTree tree) {
return isPrimitive(tree.getLeftOperand()) != isPrimitive(tree.getRightOperand());
} else {
// All BinaryTree's are of type String, a primitive type or the reference type
// equivalent of a primitive type. Furthermore, Strings don't have a primitive type, and
// equivalent of a primitive type. Furthermore, Strings don't have a primitive
// type, and
// therefore only BinaryTrees that aren't String can cause unboxing.
return !isString(tree);
}
Expand Down Expand Up @@ -772,6 +786,33 @@ public Void visitDoWhileLoop(DoWhileLoopTree tree, Void p) {
@Override
public Void visitConditionalExpression(ConditionalExpressionTree tree, Void p) {
checkForNullability(tree.getCondition(), CONDITION_NULLABLE);
// If the overall conditional expression has a primitive Java type but one or both
// operand expressions are reference types, then unboxing will occur as part of
// evaluating the conditional. In that case, check the operand(s) for possible
// nullness (unboxing.of.nullable).
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(tree);
// Only check for unboxing when javac has chosen a primitive underlying type
// for the conditional expression, but one or both operands are non-primitive.
if (type.getKind().isPrimitive()) {
ExpressionTree trueExpr = tree.getTrueExpression();
ExpressionTree falseExpr = tree.getFalseExpression();
boolean trueNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(trueExpr));
boolean falseNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(falseExpr));

if (trueNeedsUnboxing) {
if (!checkForNullability(trueExpr, UNBOXING_OF_NULLABLE)) {
// If we reported an unboxing.of.nullable error for the true arm, stop
// further checking to avoid cascading errors.
return null;
}
}
if (falseNeedsUnboxing) {
if (!checkForNullability(falseExpr, UNBOXING_OF_NULLABLE)) {
return null;
}
}
}
Comment on lines +780 to +805
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Logic is correct; consider using the existing helper method for consistency.

The fix correctly identifies when javac implicitly unboxes a reference-typed operand in a conditional expression with a primitive result type and reports unboxing.of.nullable when appropriate. The early return pattern is consistent with visitTypeCast (lines 509-513).

Minor suggestion: The existing isPrimitive(ExpressionTree) helper method (lines 714-716) could be reused for slightly more readable and consistent code.

♻️ Suggested refactor to use existing helper
     if (type.getKind().isPrimitive()) {
       ExpressionTree trueExpr = tree.getTrueExpression();
       ExpressionTree falseExpr = tree.getFalseExpression();
-      boolean trueNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(trueExpr));
-      boolean falseNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(falseExpr));
+      boolean trueNeedsUnboxing = !isPrimitive(trueExpr);
+      boolean falseNeedsUnboxing = !isPrimitive(falseExpr);
 
       if (trueNeedsUnboxing) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If the overall conditional expression has a primitive Java type but one or both
// operand expressions are reference types, then unboxing will occur as part of
// evaluating the conditional. In that case, check the operand(s) for possible
// nullness (unboxing.of.nullable).
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(tree);
// Only check for unboxing when javac has chosen a primitive underlying type
// for the conditional expression, but one or both operands are non-primitive.
if (type.getKind().isPrimitive()) {
ExpressionTree trueExpr = tree.getTrueExpression();
ExpressionTree falseExpr = tree.getFalseExpression();
boolean trueNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(trueExpr));
boolean falseNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(falseExpr));
if (trueNeedsUnboxing) {
if (!checkForNullability(trueExpr, UNBOXING_OF_NULLABLE)) {
// If we reported an unboxing.of.nullable error for the true arm, stop
// further checking to avoid cascading errors.
return null;
}
}
if (falseNeedsUnboxing) {
if (!checkForNullability(falseExpr, UNBOXING_OF_NULLABLE)) {
return null;
}
}
}
// If the overall conditional expression has a primitive Java type but one or both
// operand expressions are reference types, then unboxing will occur as part of
// evaluating the conditional. In that case, check the operand(s) for possible
// nullness (unboxing.of.nullable).
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(tree);
// Only check for unboxing when javac has chosen a primitive underlying type
// for the conditional expression, but one or both operands are non-primitive.
if (type.getKind().isPrimitive()) {
ExpressionTree trueExpr = tree.getTrueExpression();
ExpressionTree falseExpr = tree.getFalseExpression();
boolean trueNeedsUnboxing = !isPrimitive(trueExpr);
boolean falseNeedsUnboxing = !isPrimitive(falseExpr);
if (trueNeedsUnboxing) {
if (!checkForNullability(trueExpr, UNBOXING_OF_NULLABLE)) {
// If we reported an unboxing.of.nullable error for the true arm, stop
// further checking to avoid cascading errors.
return null;
}
}
if (falseNeedsUnboxing) {
if (!checkForNullability(falseExpr, UNBOXING_OF_NULLABLE)) {
return null;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java`
around lines 780 - 805, Replace the explicit primitive checks that call
TypesUtils.isPrimitive(TreeUtils.typeOf(...)) with the class's existing helper
isPrimitive(ExpressionTree) for consistency and readability inside
NullnessVisitor (the conditional-expression handling that computes
trueNeedsUnboxing/falseNeedsUnboxing); keep the rest of the logic intact (still
call checkForNullability on trueExpr/falseExpr with UNBOXING_OF_NULLABLE and
preserve the early-return behavior when checkForNullability returns false) so
only the primitive-test expressions are swapped to use
isPrimitive(ExpressionTree).


return super.visitConditionalExpression(tree, p);
}

Expand All @@ -781,19 +822,24 @@ protected void checkExceptionParameter(CatchTree tree) {
List<? extends AnnotationTree> annoTrees = param.getModifiers().getAnnotations();
Tree paramType = param.getType();
if (atypeFactory.containsNullnessAnnotation(annoTrees, paramType)) {
// This is a warning rather than an error because writing `@Nullable` could make sense
// if the catch block re-assigns the variable to null. (That would be bad style.)
// This is a warning rather than an error because writing `@Nullable` could make
// sense
// if the catch block re-assigns the variable to null. (That would be bad
// style.)
checker.reportWarning(param, "nullness.on.exception.parameter");
}

// Don't call super.
// BasetypeVisitor forces annotations on exception parameters to be top, but because
// exceptions can never be null, the Nullness Checker does not require this check.
// BasetypeVisitor forces annotations on exception parameters to be top, but
// because
// exceptions can never be null, the Nullness Checker does not require this
// check.
}

@Override
public Void visitAnnotation(AnnotationTree tree, Void p) {
// All annotation arguments are non-null and initialized, so no need to check them.
// All annotation arguments are non-null and initialized, so no need to check
// them.
return null;
}

Expand Down
16 changes: 16 additions & 0 deletions checker/tests/nullness/Issue6849.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import java.util.*;
import org.checkerframework.checker.nullness.qual.*;

public class Issue6849 {

public static <T> T m(List<T> lst) {
return lst.get(0);
}

public static void main(String[] args) {
List<@Nullable Integer> lst = new LinkedList<>();
lst.add(null);
int y = ((true) ? Issue6849.<@Nullable Integer>m(lst) : 10);
// :: error: (unboxing.of.nullable)
}
}
Loading