Skip to content

JSpecify: preserve array element nullability through requireNonNull - #1645

Open
kamilkrzywanski wants to merge 1 commit into
uber:masterfrom
kamilkrzywanski:fix-1521-requireNonNull-array-element-nullability
Open

JSpecify: preserve array element nullability through requireNonNull#1645
kamilkrzywanski wants to merge 1 commit into
uber:masterfrom
kamilkrzywanski:fix-1521-requireNonNull-array-element-nullability

Conversation

@kamilkrzywanski

@kamilkrzywanski kamilkrzywanski commented Jul 22, 2026

Copy link
Copy Markdown

Fixes #1521.

In JSpecify mode, writing null into an array element looks at the array expression's symbol to decide whether the contents are @Nullable. For an expression like Objects.requireNonNull(foo), that symbol is the method, not the original array, so element annotations from the argument type were dropped and we reported a false positive:

@NullMarked
public class Test {
  private char @Nullable [] @Nullable [] foo = null;

  void test() {
    // false positive before this PR
    Objects.requireNonNull(foo)[0] = null;
  }
}

requireNonNull only guarantees the outer array is non-null; the element type is still char @Nullable [].

The fix also consults the expression type (which keeps element annotations after generic substitution), while still checking the symbol for cases like local variables where annotations may only be present there.

Added a regression test covering the reported case and a negative case with non-null element types.

I used AI assistance while investigating and implementing this fix. I reviewed the change, confirmed the test fails without it and passes with it, and ran :nullaway:test.

Summary by CodeRabbit

  • Bug Fixes
    • Improved nullability analysis for nested and multidimensional arrays when using JSpecify annotations.
    • Array writes now produce more accurate warnings when nullable values may be assigned to non-nullable elements.
    • Improved handling of array nullability propagated through generic methods.
    • Added coverage for valid and invalid array assignments, including Objects.requireNonNull scenarios.

@CLAassistant

CLAassistant commented Jul 22, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bc4e041e-240f-48a9-aece-13bd74523ba6

📥 Commits

Reviewing files that changed from the base of the PR and between a52d773 and d408152.

📒 Files selected for processing (2)
  • nullaway/src/main/java/com/uber/nullaway/NullAway.java
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

Walkthrough

JSpecify array-write validation now derives array element nullability from the inferred expression type. NullabilityUtil adds a type-based array component check, and GenericsChecks.getTreeType becomes public for this use. Regression coverage tests nested nullable arrays, Objects.requireNonNull, generic identity propagation, and valid or invalid null assignments.

Possibly related issues

Possibly related PRs

  • uber/NullAway#1378 — Related through GenericsChecks#getTreeType type resolution.
  • uber/NullAway#1419 — Both update JSpecify array-element nullability handling and JSpecifyArrayTests.
  • uber/NullAway#1464 — Both modify GenericsChecks.getTreeType and path-aware type inference.

Suggested reviewers: msridhar, dbwiddis, yuxincs

Mergeability Score: ⚪ Minimal · up to d4081

The change preserves nullable array-element information through requireNonNull and adds regression coverage, preventing a false-positive diagnostic without introducing a supported merge-readiness concern; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the JSpecify array element nullability fix through requireNonNull.
Linked Issues check ✅ Passed The changes address the false positive for nullable multidimensional arrays wrapped with requireNonNull described in [#1521].
Out of Scope Changes check ✅ Passed The code and tests are directly related to preserving array element nullability and preventing the reported false positive.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@msridhar msridhar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the contribution! A couple thoughts below

}
boolean isElementNullable =
isArrayElementNullable(ASTHelpers.getType(arrayExpr), config)
|| (arraySymbol != null && isArrayElementNullable(arraySymbol, config));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't understand why we need this fallback? Does ASTHelpers.getType(arrayExpr) not return the right type for arrayExpr when it is a variable?

I wonder if for the general case, we need to expose a method from GenericsCheck (like getTreeType) to properly get the nullness-enhanced type of arrayExpr. I'm not sure we can rely on ASTHelpers.getType in general to preserve the right types, even if it works for the original test. E.g., does this case work?

@NullMarked
public class Test {
  private char @Nullable [] @Nullable [] foo = null;
  static <T extends @Nullable Object> T id(T t) { return t; }
  void test() {
    Objects.requireNonNull(id(foo))[0] = null;
  }  
}

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.16%. Comparing base (b67e9d3) to head (32d0d02).

Files with missing lines Patch % Lines
...away/src/main/java/com/uber/nullaway/NullAway.java 75.00% 0 Missing and 2 partials ⚠️
...c/main/java/com/uber/nullaway/NullabilityUtil.java 50.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1645      +/-   ##
============================================
- Coverage     88.17%   88.16%   -0.02%     
- Complexity     3056     3060       +4     
============================================
  Files           105      105              
  Lines         10270    10275       +5     
  Branches       2072     2075       +3     
============================================
+ Hits           9056     9059       +3     
  Misses          573      573              
- Partials        641      643       +2     

☔ View full report in Codecov by Harness.
📢 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.

isArrayElementNullable(arrayType, config)
|| (arrayType == null
&& arraySymbol != null
&& isArrayElementNullable(arraySymbol, config));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry, I still don't understand why we need this fallback to checking the symbol, can you explain?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, fair question. I don't think we need the fallback.

Dropped it and just use getTreeType for the element nullability check. Still covers requireNonNull(foo) and requireNonNull(id(foo)).

@msridhar

Copy link
Copy Markdown
Collaborator

Another potential test case here:

class Test {

  void test() {
    char @Nullable [][] x = new char @Nullable [][]{};
    x[0] = null;
    requireNonNull(new char @Nullable [][]{})[0] = null;
  }
}

Right now I get incorrect warnings for both x[0] = null and also requireNonNull(...)[0] = null.

@msridhar

Copy link
Copy Markdown
Collaborator

@kamilkrzywanski are you still able to work on this one?

@msridhar msridhar added pending-response Waiting on a response from the reporter / author and removed pending-response Waiting on a response from the reporter / author labels Aug 11, 2026

@msridhar msridhar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Posting a review to make clear we're waiting on a response to #1645 (comment)

@kamilkrzywanski

Copy link
Copy Markdown
Author

@msridhar I'll try check this tomorrow

@kamilkrzywanski
kamilkrzywanski force-pushed the fix-1521-requireNonNull-array-element-nullability branch from c7c010d to a52d773 Compare August 12, 2026 17:30
@kamilkrzywanski

Copy link
Copy Markdown
Author

@msridhar added tests for the multi-dim case you mentioned.

For char @Nullable [][], I think the warning on x[0] = null is actually right. That marks the array as nullable, not the elements. Nullable elements would be char [] @Nullable [], and that case is clean (including with requireNonNull).

Let me know if you meant something else.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java`:
- Around line 861-863: Remove the top-level `@Nullable` annotations from the local
array-reference variables in JSpecifyArrayTests.java at lines 861-863, 887-889,
and 892-895. Move the nullable array-reference declarations to fields or
parameters while preserving the existing array-content nullability and test
behavior; NullAway should infer local-variable nullability.
- Around line 874-890: In nullableArrayOfNonNullInnerArrays and
nullableOuterElementsOf2dArray, swap the multidimensional array declarations so
a uses the layout with nullable outer elements and b uses the layout with
non-null char[] elements. Update the associated BUG diagnostic expectations
accordingly: a[0] assignments remain valid, while both b[0] assignments must
expect the nullable-write diagnostic.
🪄 Autofix

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 Plus

Run ID: 3b328fb0-741e-40d0-80bb-52e0e0d5f9aa

📥 Commits

Reviewing files that changed from the base of the PR and between c7c010d and a52d773.

📒 Files selected for processing (2)
  • nullaway/src/main/java/com/uber/nullaway/NullAway.java
  • nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java

Comment on lines +874 to +890
// char @Nullable [][] : the array reference is @Nullable; elements (char[]) are
// @NonNull. Writing null into a[0] must still be reported.
void nullableArrayOfNonNullInnerArrays() {
char @Nullable [][] a = new char @Nullable [][]{};
// BUG: Diagnostic contains: Writing @Nullable expression into array with @NonNull contents
a[0] = null;
// BUG: Diagnostic contains: Writing @Nullable expression into array with @NonNull contents
Objects.requireNonNull(new char @Nullable [][]{})[0] = null;
}

// char [] @Nullable [] : outer elements are @Nullable char[]. Writes of null OK.
// (Syntax suggested by reviewer for multi-dim element nullability.)
void nullableOuterElementsOf2dArray() {
char [] @Nullable [] b = new char [] @Nullable []{};
b[0] = null;
Objects.requireNonNull(new char [] @Nullable []{})[0] = 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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 \
  'char `@Nullable` \[\]\[\]|char \[\] `@Nullable` \[\]|requireNonNull\(new char' \
  nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java

Repository: uber/NullAway

Length of output: 1440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- surrounding test ---'
sed -n '820,905p' nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java

printf '%s\n' '--- related multidimensional array cases ---'
rg -n -C 4 \
  'Nullable.*\[\].*\[\]|NonNull.*\[\].*\[\]|2d|multi.?dim|array with' \
  nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java \
  nullaway/src/test/java/com/uber/nullaway/jspecify

printf '%s\n' '--- annotation declarations and array-type guidance in the repository ---'
rg -n -C 3 \
  'char `@Nullable`|Object `@Nullable`|`@Nullable` \[\]|`@Nullable`\[\]|array reference|array component|array element' \
  nullaway/src/test nullaway/src/main | head -250

Repository: uber/NullAway

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Read-only lexical verifier for Java's dimension-annotation placement.
# For a type written as T D1 D2, the leftmost dimension is the component
# array (the inner dimension), and the rightmost dimension is the enclosing
# array (the outer dimension).
types = {
    "char `@Nullable` [][]": ["`@Nullable`", None],
    "char [] `@Nullable` []": [None, "`@Nullable`"],
}
for source, dimensions in types.items():
    inner, outer = dimensions
    print(f"{source}")
    print(f"  inner char[] dimension annotation: {inner or '<none>'}")
    print(f"  outer enclosing-array dimension annotation: {outer or '<none>'}")
    print(f"  outer elements nullable: {inner == '`@Nullable`'}")
    print(f"  outer array reference nullable: {outer == '`@Nullable`'}")
PY

Repository: uber/NullAway

Length of output: 536


Fix the inverted multidimensional array layouts.

char @nullable [][] has nullable outer elements, so both a[0] = null writes are valid. char [] @nullable [] has non-null char[] elements, so both b[0] = null writes must report the nullable-write diagnostic. Swap the declarations and expectations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java`
around lines 874 - 890, In nullableArrayOfNonNullInnerArrays and
nullableOuterElementsOf2dArray, swap the multidimensional array declarations so
a uses the layout with nullable outer elements and b uses the layout with
non-null char[] elements. Update the associated BUG diagnostic expectations
accordingly: a[0] assignments remain valid, while both b[0] assignments must
expect the nullable-write diagnostic.

Source: Coding guidelines

@kamilkrzywanski

Copy link
Copy Markdown
Author

@msridhar took another look:

  • removed the symbol fallback, type-only via getTreeType
  • multi-dim: char @Nullable [][] should still warn; char [] @Nullable [] is the nullable-elements case and passes

Squashed into one commit.

@msridhar

Copy link
Copy Markdown
Collaborator

@msridhar added tests for the multi-dim case you mentioned.

For char @Nullable [][], I think the warning on x[0] = null is actually right. That marks the array as nullable, not the elements. Nullable elements would be char [] @Nullable [], and that case is clean (including with requireNonNull).

Let me know if you meant something else.

I got confused about the syntax 🤦‍♂️ You are correct; thanks!

@msridhar msridhar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good! Just one minor comment. Also could you pull in the latest main branch and fix any merge conflicts?

Comment thread CHANGELOG.md Outdated
Comment on lines +4 to +8
Version Next
------------

* JSpecify: Preserve array element nullability through method invocations like requireNonNull (Fixes #1521) by @kamilkrzywanski

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove this? When I create the release notes I do it from the commit history, so this will automatically be added.

@msridhar

Copy link
Copy Markdown
Collaborator

@kamilkrzywanski by any chance does this PR also fix #1150?

When checking writes into array elements, take element nullability from
the expression type via GenericsChecks.getTreeType instead of the array
symbol. That keeps nullness through generic wrappers like requireNonNull
(and nested cases such as requireNonNull(id(foo))), which previously
lost annotations and produced false positives for nullable contents.

Also cover multi-dimensional array annotation placement in tests.

Fixes uber#1521
@kamilkrzywanski
kamilkrzywanski force-pushed the fix-1521-requireNonNull-array-element-nullability branch from a52d773 to d408152 Compare August 13, 2026 10:12
@kamilkrzywanski

Copy link
Copy Markdown
Author

@msridhar removed the CHANGELOG bit and rebased onto master (fixed a conflict around the new createErrorDescription signature).

Doesn't look like this also fixes #1150. Different code path: writes via requireNonNull vs assignment/reads on multi-dim arrays. Tests are green.

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.

JSpecify: False positive when wrapping nullable array of nullable arrays with requireNonNull

3 participants