Skip to content

JSpecify: use type on identifier when its type is a type variable#1378

Merged
msridhar merged 8 commits intomasterfrom
issue-1377
Dec 17, 2025
Merged

JSpecify: use type on identifier when its type is a type variable#1378
msridhar merged 8 commits intomasterfrom
issue-1377

Conversation

@msridhar
Copy link
Copy Markdown
Collaborator

@msridhar msridhar commented Dec 16, 2025

Fixes #1377

Previously, for identifiers, we used a hack to get their type from their symbol to handle the case of certain type use annotations being missed (probably necessary due to a javac bug somewhere). But, it turns out that hack would yield the wrong type in some cases for type variables. So, use the type on the identifier itself when its type is a type variable.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of nested annotations on generic array types so type retrieval preserves nested annotations correctly.
  • Tests

    • Added tests covering complex nested generics and annotated type arguments to validate nullability and type-parameter behavior (note: the diff shows the two tests inserted twice).

✏️ Tip: You can customize this high-level summary in your review settings.

@msridhar msridhar requested a review from yuxincs December 16, 2025 17:53
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 16, 2025

Walkthrough

The PR changes GenericsChecks.getTreeType so IdentifierTree branches initialize the result using ASTHelpers.getType(tree) and only fall back to the symbol.type in guarded cases (avoiding overriding tree-derived types and preserving type-use/nested annotations, while retaining prior behavior for TypeVar cases). No other control flow changes in that method. The PR also adds two new tests (issue1377 and annotationsOnTypeVariableTypeArgs) to nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java; the diff shows each test inserted twice.

Possibly related PRs

Suggested reviewers

  • yuxincs
  • lazaroclapp

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: narrowing the hack for obtaining types from identifiers to handle type variables correctly.
Linked Issues check ✅ Passed The PR addresses the core objective from #1377: fixing the NPE with intersection bounds by narrowing the hack that was producing incorrect types for type variables.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the type variable handling issue in GenericsChecks and adding regression tests; no out-of-scope modifications present.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1377

📜 Recent 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 17f215d and 0f7947f.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: msridhar
Repo: uber/NullAway PR: 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.
Learnt from: msridhar
Repo: uber/NullAway PR: 1316
File: jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/NullnessAnnotationSerializer.java:261-293
Timestamp: 2025-10-29T23:56:18.236Z
Learning: In NullAway's jdk-javac-plugin NullnessAnnotationSerializer, type variable bounds with annotations (e.g., `T extends Nullable Object`) are checked at their declaration sites by the typeParamHasAnnotation method for both class-level and method-level type parameters. The hasJSpecifyAnnotationDeep method is designed to check type uses (return types, parameters, etc.) and does not need a TYPEVAR case because type variable declaration bounds are already handled separately.
Learnt from: msridhar
Repo: uber/NullAway PR: 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-28T04:54:20.953Z
Learnt from: msridhar
Repo: uber/NullAway PR: 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/generics/GenericsChecks.java
📚 Learning: 2025-10-29T23:56:18.236Z
Learnt from: msridhar
Repo: uber/NullAway PR: 1316
File: jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/NullnessAnnotationSerializer.java:261-293
Timestamp: 2025-10-29T23:56:18.236Z
Learning: In NullAway's jdk-javac-plugin NullnessAnnotationSerializer, type variable bounds with annotations (e.g., `T extends Nullable Object`) are checked at their declaration sites by the typeParamHasAnnotation method for both class-level and method-level type parameters. The hasJSpecifyAnnotationDeep method is designed to check type uses (return types, parameters, etc.) and does not need a TYPEVAR case because type variable declaration bounds are already handled separately.

Applied to files:

  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
⏰ 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 spring-framework with snapshot
  • 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
🔇 Additional comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

462-469: The fix is correct and properly scoped.

The implementation correctly addresses issue #1377 by preserving tree types for type variables with intersection bounds while maintaining the symbol.type fallback for all other identifiers, including arrays. This approach doesn't need an explicit array type check because arrays are not type variables and naturally receive the symbol.type workaround that was originally intended for nested annotations. The broader non-TypeVar scope is intentional and more elegant than explicitly checking result.getKind() == TypeKind.ARRAY.


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 ce56ccd and 133a34a.

📒 Files selected for processing (2)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1 hunks)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: msridhar
Repo: uber/NullAway PR: 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.
Learnt from: msridhar
Repo: uber/NullAway PR: 1316
File: jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/NullnessAnnotationSerializer.java:261-293
Timestamp: 2025-10-29T23:56:18.236Z
Learning: In NullAway's jdk-javac-plugin NullnessAnnotationSerializer, type variable bounds with annotations (e.g., `T extends Nullable Object`) are checked at their declaration sites by the typeParamHasAnnotation method for both class-level and method-level type parameters. The hasJSpecifyAnnotationDeep method is designed to check type uses (return types, parameters, etc.) and does not need a TYPEVAR case because type variable declaration bounds are already handled separately.
📚 Learning: 2025-08-28T04:54:20.953Z
Learnt from: msridhar
Repo: uber/NullAway PR: 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/test/java/com/uber/nullaway/jspecify/GenericsTests.java
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
📚 Learning: 2025-08-14T18:50:06.159Z
Learnt from: msridhar
Repo: uber/NullAway PR: 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/GenericsTests.java
📚 Learning: 2025-10-29T23:56:18.236Z
Learnt from: msridhar
Repo: uber/NullAway PR: 1316
File: jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/NullnessAnnotationSerializer.java:261-293
Timestamp: 2025-10-29T23:56:18.236Z
Learning: In NullAway's jdk-javac-plugin NullnessAnnotationSerializer, type variable bounds with annotations (e.g., `T extends Nullable Object`) are checked at their declaration sites by the typeParamHasAnnotation method for both class-level and method-level type parameters. The hasJSpecifyAnnotationDeep method is designed to check type uses (return types, parameters, etc.) and does not need a TYPEVAR case because type variable declaration bounds are already handled separately.

Applied to files:

  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
⏰ 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 spring-framework with snapshot
  • GitHub Check: Build and test on windows-latest
  • GitHub Check: Build caffeine with snapshot
🔇 Additional comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

462-467: LGTM! Narrowing the type retrieval hack appropriately.

The change correctly narrows the special-case type retrieval to only array-typed identifiers, which should fix the type variable issue mentioned in #1377 while preserving the workaround for nested array annotations. The fallback to symbol.type is now conditional on result.getKind() == TypeKind.ARRAY, which aligns with the PR objectives.

Comment on lines +2756 to +2784
@Test
public void issue1377() {
makeHelper()
.addSourceLines(
"Test.java",
"import org.jspecify.annotations.NullMarked;",
"@NullMarked",
"class Test {",
"interface Marker {}",
"class Generic<T> {",
" public void method() {}",
"}",
"class Base<T extends Object & Marker> {",
" T instance;",
" Base(T instance) {",
" this.instance = instance;",
" }",
"}",
"class SubClass<T extends Generic<Integer> & Marker> extends Base<T> {",
" SubClass(T instance) {",
" super(instance);",
" }",
" void method() {",
" instance.method();",
" }",
"}",
"}")
.doTest();
}
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

Regression test looks good, consider adding explanatory comment.

The test appropriately covers the intersection bound scenario from issue #1377, ensuring NullAway doesn't throw an NPE when analyzing method invocations on inherited generic fields with intersection-bounded type parameters (e.g., T extends Generic<Integer> & Marker).

Consider adding a brief comment at the beginning of the test method to document that this is a regression test for issue #1377:

 @Test
 public void issue1377() {
+  // Regression test for https://github.com/uber/NullAway/issues/1377
+  // Ensures no NPE when analyzing intersection-bounded type variables in JSpecify mode
   makeHelper()

This would help future maintainers understand the test's purpose.

🤖 Prompt for AI Agents
In nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java around
lines 2756 to 2784, add a brief Java comment at the start of the issue1377()
test method (immediately before the makeHelper() call) noting that this is a
regression test for issue #1377 and explaining concisely that it verifies
NullAway does not NPE when analyzing method invocations on inherited generic
fields with intersection-bounded type parameters (e.g., T extends
Generic<Integer> & Marker). This comment should be one or two lines, mention the
specific scenario being tested and the bug being prevented, and then keep the
rest of the test unchanged.

@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 66.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.46%. Comparing base (ce56ccd) to head (0f7947f).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...ava/com/uber/nullaway/generics/GenericsChecks.java 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1378      +/-   ##
============================================
- Coverage     88.47%   88.46%   -0.01%     
- Complexity     2618     2619       +1     
============================================
  Files            97       97              
  Lines          8772     8774       +2     
  Branches       1750     1751       +1     
============================================
+ Hits           7761     7762       +1     
  Misses          504      504              
- Partials        507      508       +1     

☔ 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.

@msridhar msridhar changed the title Narrow hack to get proper type for array variables JSpecify: use type on identifier when its type is a type variable Dec 16, 2025
Copy link
Copy Markdown
Collaborator

@yuxincs yuxincs left a comment

Choose a reason for hiding this comment

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

Makes sense to me!

@msridhar msridhar merged commit 03833a5 into master Dec 17, 2025
8 of 11 checks passed
@msridhar msridhar deleted the issue-1377 branch December 17, 2025 16:28
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.

Unhandled NullPointerException with intersection bounds on generic field

2 participants