Skip to content

Conversation

@dhruv-agr
Copy link
Contributor

@dhruv-agr dhruv-agr commented Sep 8, 2025

The core of the solution is a new check that activates when the loop variable is a primitive type
For arrays (e.g., @nullable Integer[]), the existing NullabilityUtil.isArrayElementNullable utility is used to determine if the elements can be null.

Summary by CodeRabbit

  • New Features

    • Adds a JSpecify-only check that flags unsafe auto-unboxing in enhanced for-each loops over arrays when elements may be nullable, emitting a clear diagnostic; no behavior change outside JSpecify mode.
  • Tests

    • Added tests for nullable and non-null array elements in for-each unboxing scenarios.
    • Introduced a test-only Nullable annotation and additional cases validating array element nullability behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Walkthrough

Adds a JSpecify-mode check in EnhancedForLoop processing that reports UNBOX_NULLABLE when a primitive loop variable would unbox potentially @nullable array elements; adds tests covering nullable and non-null array element cases and a test-only TYPE_USE @Nullable annotation.

Changes

Cohort / File(s) Summary
Core analyzer logic
nullaway/src/main/java/com/uber/nullaway/NullAway.java
In JSpecify mode, EnhancedForLoop handling now checks if the loop variable is primitive and the iterated expression is an array whose element type may be nullable; if so, reports UNBOX_NULLABLE on the loop variable. No public API signature changes.
JSpecify tests & test annotation
nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java, .../Nullable.java
Adds tests: unboxForEachLoop (expects unboxing diagnostic for @Nullable Integer[]) and unboxForEachLoopNonNull (no diagnostic for non-null Integer[]), plus a test-only com.uber.Nullable TYPE_USE annotation and related tests asserting nullable-element/dereference diagnostics.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Visitor/Analyzer
  participant NullAway as NullAway.enhancedForLoop
  participant Config as Config (JSpecifyMode)
  participant Types as ASTHelpers/Types

  Caller->>NullAway: visitEnhancedForLoop(node)
  NullAway->>Config: isJSpecifyMode()
  alt Not JSpecify
    NullAway-->>Caller: NO_MATCH / continue existing checks
  else JSpecify
    NullAway->>Types: getType(loopVariable)
    alt loopVariable is primitive
      NullAway->>Types: getType(loopExpression)
      alt expression is array
        NullAway->>Types: resolve array symbol / isArrayElementNullable(..., config)
        alt elements nullable
          Note right of NullAway #lightblue: New UNBOX_NULLABLE path
          NullAway-->>Caller: report UNBOX_NULLABLE ("unboxing of a @Nullable value")
        else elements non-null
          NullAway-->>Caller: NO_MATCH / continue existing checks
        end
      else not array
        NullAway-->>Caller: continue existing logic
      end
    else non-primitive
      NullAway-->>Caller: continue existing logic
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • yuxincs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title directly describes the primary change — reporting an unboxing warning for for-each loops — and matches the PR's implemented behavior, so it is relevant and sufficiently specific for a reviewer to understand the main intent. Including the issue number is minor noise but does not make the title misleading or unrelated.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4722a4c and a553550.

📒 Files selected for processing (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

@msridhar msridhar marked this pull request as ready for review September 8, 2025 16:25
@codecov
Copy link

codecov bot commented Sep 8, 2025

Codecov Report

❌ Patch coverage is 78.57143% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.43%. Comparing base (533986c) to head (a553550).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...away/src/main/java/com/uber/nullaway/NullAway.java 78.57% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1281      +/-   ##
============================================
- Coverage     88.45%   88.43%   -0.02%     
- Complexity     2454     2457       +3     
============================================
  Files            92       92              
  Lines          8100     8114      +14     
  Branches       1610     1614       +4     
============================================
+ Hits           7165     7176      +11     
  Misses          472      472              
- Partials        463      466       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
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: 2

🧹 Nitpick comments (1)
nullaway/src/test/java/com/uber/nullaway/CoreTests.java (1)

1176-1191: Pre-wire the expected diagnostic for the Iterable case (kept @ignore).

When you un-ignore this, assert the unboxing diagnostic so the intent is explicit.

   @Ignore
   @Test
   public void unboxForEachLoopIterable() {
     defaultCompilationHelper
         .addSourceLines(
             "Test.java",
             "import org.jspecify.annotations.*;",
             "import java.util.List;",
             "@NullMarked",
             "class Test {",
             "   void f(List<@Nullable Integer> numbers) {",
+            "     // BUG: Diagnostic contains: unboxing of a @Nullable value",
             "     for (int x : numbers) {}",
             "   }",
             "}")
         .doTest();
   }

If you want, I can implement the Iterable path in NullAway.java to make this pass (inspect generic element nullness in JSpecify mode).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f40500 and bc2eebe.

📒 Files selected for processing (2)
  • nullaway/src/main/java/com/uber/nullaway/NullAway.java (2 hunks)
  • nullaway/src/test/java/com/uber/nullaway/CoreTests.java (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-28T04:54:20.953Z
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.

Applied to files:

  • nullaway/src/main/java/com/uber/nullaway/NullAway.java
📚 Learning: 2025-08-14T18:50:06.159Z
Learnt from: msridhar
PR: uber/NullAway#1245
File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102
Timestamp: 2025-08-14T18:50:06.159Z
Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.

Applied to files:

  • nullaway/src/test/java/com/uber/nullaway/CoreTests.java
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/NullAway.java (1)
nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java (1)
  • NullabilityUtil (66-665)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build and test on ubuntu-latest
  • GitHub Check: Build and test on macos-latest
  • GitHub Check: Build and test on windows-latest
  • GitHub Check: Build caffeine with snapshot
  • GitHub Check: Build spring-framework with snapshot
🔇 Additional comments (2)
nullaway/src/main/java/com/uber/nullaway/NullAway.java (1)

95-95: LGTM: import is correct and used.

ArrayType is required for component-type inspection in the new for-each logic.

nullaway/src/test/java/com/uber/nullaway/CoreTests.java (1)

26-26: LGTM: Ignore import is fine.

Used by the iterable test below.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Thanks for the fix!

if (mayBeNullExpr(state, expr)) {
return errorBuilder.createErrorDescription(errorMessage, buildDescription(expr), state, null);
}
// auto-unboxing check
Copy link
Collaborator

Choose a reason for hiding this comment

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

before doing this check, we should bail out if config.isJSpecifyMode() returns false. We only want to do this check in JSpecify mode

return Description.NO_MATCH;
}
Type expressionType = ASTHelpers.getType(expr);
boolean isElementNullable = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need this variable. Just move the logic for reporting the error within the innermost if

"class Test {",
" void f(@Nullable Integer[] array) {",
" // BUG: Diagnostic contains:unboxing of a @Nullable value",
" for (int x : array) {}",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a negative test, to ensure we don't get a warning when unboxing elements of an array that doesn't have @Nullable contents

.doTest();
}

// @Ignore
Copy link
Collaborator

Choose a reason for hiding this comment

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

delete

Comment on lines 1176 to 1191
@Ignore
@Test
public void unboxForEachLoopIterable() {
defaultCompilationHelper
.addSourceLines(
"Test.java",
"import org.jspecify.annotations.*;",
"import java.util.List;",
"@NullMarked",
"class Test {",
" void f(List<@Nullable Integer> numbers) {",
" for (int x : numbers) {}",
" }",
"}")
.doTest();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Delete this new test. We'll add it later when we handle iterables


// @Ignore
@Test
public void unboxForEachLoop() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test belongs in JSpecifyArrayTests. It should only report an error in JSpecify mode.

Copy link
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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18c3de2 and 4722a4c.

📒 Files selected for processing (2)
  • nullaway/src/main/java/com/uber/nullaway/NullAway.java (1 hunks)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • nullaway/src/main/java/com/uber/nullaway/NullAway.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: msridhar
PR: uber/NullAway#1245
File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102
Timestamp: 2025-08-14T18:50:06.159Z
Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.
📚 Learning: 2025-08-14T18:50:06.159Z
Learnt from: msridhar
PR: uber/NullAway#1245
File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102
Timestamp: 2025-08-14T18:50:06.159Z
Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.

Applied to files:

  • nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java

Comment on lines 675 to 689
@Test
public void unboxForEachLoop() {
makeHelper()
.addSourceLines(
"Outer.java",
"import org.jspecify.annotations.*;",
"@NullMarked",
"class Test {",
" void f(@Nullable Integer[] array) {",
" // BUG: Diagnostic contains:unboxing of a @Nullable value",
" for (int x : array) {}",
" }",
"}")
.doTest();
}
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 12, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add package declaration so NullAway runs; fix BUG marker spacing

These snippets lack package com.uber;. Since the helper sets -XepOpt:NullAway:AnnotatedPackages=com.uber, the analyzer won’t run on default-package sources and the expected diagnostic won’t be produced. Also, keep the “BUG: Diagnostic contains:” formatting consistent with existing tests (space after colon).

             "Outer.java",
-            "import org.jspecify.annotations.*;",
+            "package com.uber;",
+            "import org.jspecify.annotations.*;",
             "@NullMarked",
             "class Test {",
             "   void f(@Nullable Integer[] array) {",
-            "   // BUG: Diagnostic contains:unboxing of a @Nullable value",
+            "   // BUG: Diagnostic contains: unboxing of a @Nullable value",
             "     for (int x : array) {}",
             "   }",
             "}")

Optional: for consistency with the rest of this class, consider naming the file "Test.java" instead of "Outer.java".

📝 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
@Test
public void unboxForEachLoop() {
makeHelper()
.addSourceLines(
"Outer.java",
"import org.jspecify.annotations.*;",
"@NullMarked",
"class Test {",
" void f(@Nullable Integer[] array) {",
" // BUG: Diagnostic contains:unboxing of a @Nullable value",
" for (int x : array) {}",
" }",
"}")
.doTest();
}
@Test
public void unboxForEachLoop() {
makeHelper()
.addSourceLines(
"Outer.java",
"package com.uber;",
"import org.jspecify.annotations.*;",
"@NullMarked",
"class Test {",
" void f(@Nullable Integer[] array) {",
" // BUG: Diagnostic contains: unboxing of a @Nullable value",
" for (int x : array) {}",
" }",
"}")
.doTest();
}
🤖 Prompt for AI Agents
In nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java
around lines 675-689, the test source is in the default package so NullAway
won't analyze it; add the package declaration "package com.uber;" at the top of
the generated source string so the test is in the AnnotatedPackages scope, and
adjust the BUG marker to include a space after the colon ("// BUG: Diagnostic
contains: unboxing of a @Nullable value") to match existing test formatting;
optionally rename the virtual filename from "Outer.java" to "Test.java" for
consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The package is not necessary. Since the class is marked as @NullMarked, NullAway will run.

Copy link
Contributor

Choose a reason for hiding this comment

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

@msridhar Thank you for the correction! You're absolutely right. Looking at the retrieved learnings, I can see that classes annotated with @NullMarked are analyzed by NullAway even if they are not in packages specified by the AnnotatedPackages configuration - the @NullMarked annotation guarantees NullAway analysis.

So the package declaration is indeed unnecessary. However, the spacing issue in the BUG marker is still worth fixing for consistency:

-            "   // BUG: Diagnostic contains:unboxing of a @Nullable value",
+            "   // BUG: Diagnostic contains: unboxing of a @Nullable value",

This matches the formatting used throughout the rest of the test file.


🧠 Learnings used
Learnt from: msridhar
PR: uber/NullAway#1245
File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102
Timestamp: 2025-08-14T18:50:06.159Z
Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.

Learnt from: msridhar
PR: uber/NullAway#1259
File: jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/SwitchTests.java:318-321
Timestamp: 2025-08-29T18:41:43.584Z
Learning: Classes annotated with NullMarked are analyzed by NullAway even if they are not in packages specified by the AnnotatedPackages configuration. The NullMarked annotation guarantees NullAway analysis.

Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.

Comment on lines 691 to 704
@Test
public void unboxForEachLoopNonNull() {
makeHelper()
.addSourceLines(
"Outer.java",
"import org.jspecify.annotations.*;",
"@NullMarked",
"class Test {",
" void f(Integer[] array) {",
" for (int x : array) {}",
" }",
"}")
.doTest();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add package declaration so the negative test is analyzed

Same package issue here; without package com.uber; this compiles outside annotated packages and may mask regressions.

             "Outer.java",
-            "import org.jspecify.annotations.*;",
+            "package com.uber;",
+            "import org.jspecify.annotations.*;",
             "@NullMarked",
             "class Test {",
             "   void f(Integer[] array) {",
             "     for (int x : array) {}",
             "   }",
             "}")

If the implementation also intends to cover enhanced-for over Iterable<@Nullable Integer>, add a companion test to lock that behavior in.

📝 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
@Test
public void unboxForEachLoopNonNull() {
makeHelper()
.addSourceLines(
"Outer.java",
"import org.jspecify.annotations.*;",
"@NullMarked",
"class Test {",
" void f(Integer[] array) {",
" for (int x : array) {}",
" }",
"}")
.doTest();
}
@Test
public void unboxForEachLoopNonNull() {
makeHelper()
.addSourceLines(
"Outer.java",
"package com.uber;",
"import org.jspecify.annotations.*;",
"@NullMarked",
"class Test {",
" void f(Integer[] array) {",
" for (int x : array) {}",
" }",
"}")
.doTest();
}
🤖 Prompt for AI Agents
In nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java
around lines 691 to 704, the test source lacks a package declaration which
causes it to compile outside the annotated package and may hide regressions; add
"package com.uber;" as the first line inside the added source so the negative
test is analyzed in the intended annotated package, and if you also want to lock
behavior for enhanced-for over Iterable<@Nullable Integer>, add a companion test
method that supplies an Iterable<@Nullable Integer> source and asserts the same
unboxing/for-each behavior.

@msridhar msridhar enabled auto-merge (squash) September 12, 2025 22:30
@msridhar msridhar merged commit dd0da6d into uber:master Sep 12, 2025
8 of 11 checks passed
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.

2 participants