Skip to content

Fix: Report unboxing.of.nullable in conditional expressions with primitive result#7433

Open
Suvrat1629 wants to merge 8 commits intotypetools:masterfrom
Suvrat1629:false-neg-6849
Open

Fix: Report unboxing.of.nullable in conditional expressions with primitive result#7433
Suvrat1629 wants to merge 8 commits intotypetools:masterfrom
Suvrat1629:false-neg-6849

Conversation

@Suvrat1629
Copy link
Copy Markdown

  • Fixes false negative where a @Nullable boxed value in a ternary expression with primitive result type was unboxed without warning.
  • Added check in NullnessVisitor.visitConditionalExpression to detect when javac performs unboxing conversion on one or both operands.
  • Added regression test Issue6849.java.

Closes #6849

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba65df82-b99e-4204-896c-2ff7075f0a85

📥 Commits

Reviewing files that changed from the base of the PR and between 8d51e2f and c885fc1.

📒 Files selected for processing (1)
  • docs/manual/contributors.tex

📝 Walkthrough

Walkthrough

The visitor method visitConditionalExpression in NullnessVisitor was extended to, when the conditional expression has a primitive target type and one or both arms are reference types, invoke checkForNullability on the relevant arm(s) with the unboxing.of.nullable check and return early if that check reports an error. A new test Issue6849.java was added demonstrating a ternary expression combining a nullable generic reference and a primitive constant to exercise this unboxing nullness check.

Possibly related PRs

Suggested reviewers

  • smillst
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully addresses issue #6849 by adding nullness checking for unboxing in conditional expressions with primitive result types.
Out of Scope Changes check ✅ Passed All changes are scoped to addressing issue #6849: NullnessVisitor enhancement, regression test file, and contributor documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11643b3 and 0994c6a.

📒 Files selected for processing (2)
  • checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java
  • checker/tests/nullness/Issue6849.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: kelloggm
Repo: typetools/checker-framework PR: 7166
File: checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java:470-515
Timestamp: 2025-10-22T20:40:48.819Z
Learning: In MustCallVisitor.java (Checker Framework), prefer using Name.equals(...) or Objects.equals(...) for com.sun.source.util.Name comparisons (instead of ==/!=). This should be the default unless the Interning Checker is explicitly used to guarantee reference equality.
📚 Learning: 2025-10-22T20:40:48.819Z
Learnt from: kelloggm
Repo: typetools/checker-framework PR: 7166
File: checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java:470-515
Timestamp: 2025-10-22T20:40:48.819Z
Learning: In MustCallVisitor.java (Checker Framework), prefer using Name.equals(...) or Objects.equals(...) for com.sun.source.util.Name comparisons (instead of ==/!=). This should be the default unless the Interning Checker is explicitly used to guarantee reference equality.

Applied to files:

  • checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java
🧬 Code graph analysis (1)
checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java (1)
javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java (1)
  • TypesUtils (55-1555)
🔇 Additional comments (13)
checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java (12)

71-72: Formatting-only change — no functional impact.


144-146: Formatting-only change — no functional impact.


157-163: Formatting-only change — no functional impact.


192-196: Formatting-only change — no functional impact.


221-226: Formatting-only change — no functional impact.


394-400: Formatting-only change — no functional impact.


442-444: Formatting-only change — no functional impact.


571-571: Formatting-only change — no functional impact.


699-701: Formatting-only change — no functional impact.


825-836: Formatting-only change — no functional impact.


841-842: Formatting-only change — no functional impact.


787-816: Logic correctly detects unboxing in conditional expressions, verified by existing tests.

The implementation properly:

  1. Checks if the overall conditional result type is primitive
  2. Identifies operands that need unboxing (reference types being assigned to primitive)
  3. Reports unboxing.of.nullable when nullable references would be unboxed
  4. Returns early after reporting an error to avoid cascading errors (consistent with visitTypeCast pattern)

The new logic is exercised by existing tests in Issue6849.java and Issue3614.java, which verify that unboxing errors are correctly reported in conditional expressions (e.g., int y = ((true) ? method() : 10) when method returns @Nullable Integer).

When both operands need unboxing and both are nullable, only the first error is reported due to the early return. This is acceptable—the cascading error prevention is intentional, though users would need to re-run after fixing the first issue to see subsequent errors.

checker/tests/nullness/Issue6849.java (1)

6-8: Consider adding @Nullable annotation to the return type for explicitness.

The method m returns T, which when instantiated as @Nullable Integer correctly returns a nullable value. The current implementation is correct, but you might consider whether adding a test case that explicitly shows the return type could help document the behavior.

Comment thread checker/tests/nullness/Issue6849.java
@smillst smillst self-assigned this Jan 15, 2026
@Suvrat1629
Copy link
Copy Markdown
Author

@smillst gentle ping please take a look

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java`:
- Around line 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).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f60011f and 164f5e6.

📒 Files selected for processing (1)
  • checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java

Comment on lines +780 to +805
// 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;
}
}
}
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).

@mernst
Copy link
Copy Markdown
Member

mernst commented Apr 22, 2026

@Suvrat1629 Could you please respond to the code review from CodeRabbit?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java (1)

784-785: 🧹 Nitpick | 🔵 Trivial

Reuse the existing isPrimitive(ExpressionTree) helper for consistency.

Lines 784–785 re-implement what the private helper isPrimitive(ExpressionTree) at line 708 already does. Using the helper keeps this method consistent with visitTypeCast (line 507) and isUnboxingOperation (line 682).

♻️ Proposed refactor
-      boolean trueNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(trueExpr));
-      boolean falseNeedsUnboxing = !TypesUtils.isPrimitive(TreeUtils.typeOf(falseExpr));
+      boolean trueNeedsUnboxing = !isPrimitive(trueExpr);
+      boolean falseNeedsUnboxing = !isPrimitive(falseExpr);
🤖 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 784 - 785, Replace the ad-hoc primitive checks for
trueNeedsUnboxing and falseNeedsUnboxing by calling the existing private helper
isPrimitive(ExpressionTree) instead of
TypesUtils.isPrimitive(TreeUtils.typeOf(...)); update the assignments for
trueNeedsUnboxing and falseNeedsUnboxing to use isPrimitive(trueExpr) and
isPrimitive(falseExpr) so this logic matches the other usages in visitTypeCast
and isUnboxingOperation and keeps behavior consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java`:
- Around line 787-798: In NullnessVisitor, when checking the conditional arms,
don't early-return after checkForNullability(trueExpr, UNBOXING_OF_NULLABLE);
instead always call checkForNullability on both trueExpr and falseExpr (using
UNBOXING_OF_NULLABLE) and record each result, then return null if either check
failed; mirror the pattern used in visitBinary so both operands can report
unboxing errors while still stopping further processing if any failed. Ensure
you reference the existing trueNeedsUnboxing/falseNeedsUnboxing guards and keep
the existing behavior of returning null when any arm's check fails.

---

Duplicate comments:
In
`@checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java`:
- Around line 784-785: Replace the ad-hoc primitive checks for trueNeedsUnboxing
and falseNeedsUnboxing by calling the existing private helper
isPrimitive(ExpressionTree) instead of
TypesUtils.isPrimitive(TreeUtils.typeOf(...)); update the assignments for
trueNeedsUnboxing and falseNeedsUnboxing to use isPrimitive(trueExpr) and
isPrimitive(falseExpr) so this logic matches the other usages in visitTypeCast
and isUnboxingOperation and keeps behavior consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a82e6db6-7e99-4027-b8ab-a0b94a998ee1

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba7018 and 8d51e2f.

📒 Files selected for processing (1)
  • checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java

Comment on lines +787 to +798
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;
}
}
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.

⚠️ Potential issue | 🟡 Minor

Early return after true arm error skips checking the false arm.

If the true arm fails the nullability check, the method returns before checking falseExpr, so a second unboxing error on the false arm is suppressed in the same conditional. visitTypeCast (lines 507–513) has only one operand so the early-return pattern is equivalent there, but a conditional expression has two independent operands that can each require unboxing. Consider reporting on both arms before returning, to match how visitBinary (lines 476–479) checks both operands unconditionally.

♻️ Proposed change
-      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;
-        }
-      }
+      boolean ok = true;
+      if (trueNeedsUnboxing) {
+        ok &= checkForNullability(trueExpr, UNBOXING_OF_NULLABLE);
+      }
+      if (falseNeedsUnboxing) {
+        ok &= checkForNullability(falseExpr, UNBOXING_OF_NULLABLE);
+      }
+      if (!ok) {
+        // Avoid cascading errors from the superclass visit.
+        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 787 - 798, In NullnessVisitor, when checking the conditional arms,
don't early-return after checkForNullability(trueExpr, UNBOXING_OF_NULLABLE);
instead always call checkForNullability on both trueExpr and falseExpr (using
UNBOXING_OF_NULLABLE) and record each result, then return null if either check
failed; mirror the pattern used in visitBinary so both operands can report
unboxing errors while still stopping further processing if any failed. Ensure
you reference the existing trueNeedsUnboxing/falseNeedsUnboxing guards and keep
the existing behavior of returning null when any arm's check fails.

@mernst mernst assigned Suvrat1629 and unassigned smillst Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

false negative when using ternary operator

3 participants