Skip to content

Better handle calls to super constructors and superclass methods in JSpecify mode#1248

Merged
msridhar merged 18 commits intomasterfrom
fix-1246
Aug 30, 2025
Merged

Better handle calls to super constructors and superclass methods in JSpecify mode#1248
msridhar merged 18 commits intomasterfrom
fix-1246

Conversation

@msridhar
Copy link
Copy Markdown
Collaborator

@msridhar msridhar commented Aug 16, 2025

Fixes #1246

We were computing the enclosing type for such calls incorrectly before; now we use the enclosing class of the call itself. We extract the logic for computing enclosing types to its own function and use it consistently everywhere.

This change exposed a subtle bug with type substitutions; relevant test case is nullableWildcardFromCaffeine. We fix that bug here as well, though there is still another inference bug to be fixed. Opened #1267 and will fix in a follow-up.

Summary by CodeRabbit

  • New Features
    • More precise determination of enclosing types for generic method and constructor calls, improving nullability analysis for unqualified calls.
  • Bug Fixes
    • Reduced false positives/negatives in generic parameter and return nullability checks at invocation sites.
  • Tests
    • Replaced an ignored superclass-constructor test with two targeted super-constructor nullability tests.
    • Added a new test exercising complex wildcard/substitution scenarios inspired by Caffeine.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Aug 16, 2025

Walkthrough

Derives the enclosing class/type for generic nullability checks at call sites via a new getEnclosingTypeForCallExpression(...); centralizes type-arg substitution using that enclosing type for method returns and parameters; refactors substituteTypeArgsInGenericMethodType signature; updates tests and adds a Caffeine-derived generic test.

Changes

Cohort / File(s) Summary
Generic nullness resolution & substitution
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
Normalize trees (stripParentheses); add ClassTree import; add getEnclosingTypeForCallExpression(...) to compute enclosing type for MethodInvocationTree/NewClassTree (use enclosing ClassTree type for unqualified calls, receiver type for member-select, null for static methods, invocation type for constructors); refactor substituteTypeArgsInGenericMethodType signature to (Tree, Type methodType, List<Type> tvars, VisitorState) and update call sites; update getGenericReturnNullnessAtInvocation and getGenericParameterNullnessAtInvocation to use the new flow.
Generics tests — nullable super scenarios changed
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java
Replace previously ignored nullableSuperConstructorArg with two focused tests (passNonNullToNullableSuperConstructorArg, mismatchedTypeArgNullabilityForSuperConstructor); retain other tests; one constructor-receiver test remains ignored under issue 836.
Add Caffeine-derived generic inference test
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
Add nullableWildcardFromCaffeine() to exercise inference/substitution involving wildcard/nullability and Type identity.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Call site (AST)
  participant GC as GenericsChecks
  participant TS as TypeSystem

  Caller->>GC: getGenericParameterNullnessAtInvocation(tree, methodSym, state)
  GC->>GC: normalize tree (stripParentheses)
  GC->>GC: getEnclosingTypeForCallExpression(methodSym, tree, state)
  alt NewClassTree (constructor)
    GC-->>TS: enclosingType = invocation.type
  else MethodInvocationTree
    alt static method
      GC-->>GC: enclosingType = null
    else selector is IdentifierTree (unqualified)
      GC->>Caller: find enclosing ClassTree
      Caller-->>TS: enclosingType = ClassTree.type
    else selector is MemberSelectTree (qualified)
      GC->>TS: enclosingType = receiver.expression.type
    end
  end
  GC->>TS: substituteTypeArgsInGenericMethodType(tree, methodType, tvars, state) using enclosingType
  TS-->>GC: resolved parameter/return nullness
  GC-->>Caller: nullness result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
Allow null super constructor argument for subclass of generic with @nullable bound (#1246)
Allow null super method argument for subclass of generic with @nullable bound (#1246)

Assessment — Out-of-scope changes

Code Change Explanation
Add Caffeine-derived test nullableWildcardFromCaffeine() (nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java) New test targets a separate generic inference/substitution scenario not specified in issue #1246; it extends coverage beyond the linked issue objectives.

Possibly related PRs

Suggested reviewers

  • lazaroclapp
  • yuxincs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aa796f0 and 97d5173.

📒 Files selected for processing (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.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). (8)
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: Build caffeine with snapshot
  • GitHub Check: Build spring-framework with snapshot
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-1246

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@msridhar msridhar requested a review from yuxincs August 16, 2025 00:08
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: 0

🧹 Nitpick comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

1136-1155: Correctly derive enclosing type for unqualified and super calls; two small refinements

This fixes the core issue by using the enclosing class for IdentifierTree (this/super) calls and the receiver for MemberSelectTree. Early return for static methods is also correct.

Two small improvements:

  • Prefer ASTHelpers.getSymbol(enclosingClassTree).type over casting to JCTree to reduce reliance on javac internals.
  • Strip parentheses from the MemberSelect receiver as well (symmetry with the methodSelect strip), even if it’s typically harmless.

Apply this diff within this block:

-        ClassTree enclosingClassTree =
-            ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class);
-        if (enclosingClassTree != null) {
-          enclosingType = ((JCTree.JCClassDecl) enclosingClassTree).type;
-        }
+        ClassTree enclosingClassTree =
+            ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class);
+        if (enclosingClassTree != null) {
+          enclosingType = ASTHelpers.getSymbol(enclosingClassTree).type;
+        }
@@
-      } else if (methodSelect instanceof MemberSelectTree) {
-        enclosingType = getTreeType(((MemberSelectTree) methodSelect).getExpression(), config);
+      } else if (methodSelect instanceof MemberSelectTree) {
+        ExpressionTree receiver =
+            ASTHelpers.stripParentheses(((MemberSelectTree) methodSelect).getExpression());
+        enclosingType = getTreeType(receiver, config);
       }

Optional follow-ups (no changes requested here):

  • Consider extracting this “compute enclosing type for an invocation” logic into a helper and reusing it in:
    • compareGenericTypeParameterNullabilityForCall (Lines 753–768 currently use methodSymbol.owner.type for IdentifierTree, which may miss subclass-instantiated type args),
    • getGenericReturnNullnessAtInvocation (Lines 1016–1026 currently skip unqualified calls).
      This would keep behavior consistent across parameter and return nullness checks.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f59d952 and e2a2f99.

📒 Files selected for processing (2)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (2 hunks)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java (0 hunks)
💤 Files with no reviewable changes (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java
🔇 Additional comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

12-12: Import for ClassTree is appropriate

Required to locate the enclosing class for unqualified/super calls. Looks good.

@codecov
Copy link
Copy Markdown

codecov bot commented Aug 16, 2025

Codecov Report

❌ Patch coverage is 97.14286% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.16%. Comparing base (cf02218) to head (97d5173).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...ava/com/uber/nullaway/generics/GenericsChecks.java 97.14% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1248      +/-   ##
============================================
- Coverage     88.17%   88.16%   -0.02%     
+ Complexity     2427     2422       -5     
============================================
  Files            92       92              
  Lines          8010     8010              
  Branches       1595     1594       -1     
============================================
- Hits           7063     7062       -1     
  Misses          492      492              
- Partials        455      456       +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 marked this pull request as draft August 16, 2025 00:13
@msridhar msridhar marked this pull request as ready for review August 16, 2025 17:12
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

🧹 Nitpick comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

1154-1154: Minor consistency: prefer static verify(...) over Verify.verify(...)

The file mixes both; use the static import consistently.

Apply this diff:

-      Verify.verify(tree instanceof NewClassTree);
+      verify(tree instanceof NewClassTree);

Additionally, since the non-static import becomes unused, remove the regular import at Line 6:

-import com.google.common.base.Verify;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e2a2f99 and 6332d88.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
  • TypeSubstitutionUtils (22-251)
⏰ 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). (6)
  • GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: Build Caffeine with snapshot
🔇 Additional comments (5)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (5)

12-12: Import addition is appropriate

ClassTree is needed for locating the enclosing class; good addition.


376-377: Normalize input by stripping parentheses

Stripping parentheses here is a solid fix to avoid losing annotations when callers forget to pre-strip. This should reduce edge-case misses for annotated types under parentheses.


996-1002: Correctly derive enclosing type for return-nullness at call sites

Leveraging getEnclosingTypeForCallExpression(...) here fixes the super and unqualified-call cases in JSpecify mode. Static-call early-exit to NONNULL is also correct (class generics don’t apply).


1112-1120: Consistent parameter-nullness derivation via enclosing type

Same improvement applied to parameters; this should address nullable super constructor/super method arguments in JSpecify mode.


1122-1160: Helper captures receiver/enclosing context for both implicit and explicit receivers

Centralizing the logic is great. A couple of edge considerations to keep in mind:

  • For methodSelect being a super-qualified select (e.g., A.super.m()), methodSelect is a MemberSelectTree; getTreeType(receiver, config) should yield the appropriate supertype with type args. This looks correct, but it’s worth validating with a qualified-super test if you don’t already have one.
  • For constructor diamond calls, getTreeType returns null (as documented). That means we’ll skip class-level substitution for those. That behavior matches the current limitation; just calling it out.

Would you like me to add a test for A.super.m(...) with @nullable type args to lock this down?

@msridhar msridhar marked this pull request as draft August 17, 2025 15:35
@msridhar msridhar marked this pull request as ready for review August 28, 2025 04:08
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

1169-1189: Do parameter nullness composition in one pass (class-level then method-level).

Same rationale as for return nullness: start from memberType when available, then apply explicit/inferred method type args onto that before checking the parameter.

Apply:

-    // If generic method invocation
-    if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
-      // Substitute the argument types within the MethodType
-      Type.ForAll forAllType = (Type.ForAll) invokedMethodSymbol.type;
-      List<Type> substitutedParamTypes =
-          substituteTypeArgsInGenericMethodType(
-                  tree, forAllType.qtype.asMethodType(), forAllType.tvars, state)
-              .getParameterTypes();
-      // If this condition evaluates to false, we fall through to the subsequent logic, to handle
-      // type variables declared on the enclosing class
-      if (substitutedParamTypes != null
-          && Objects.equals(
-              getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam),
-              Nullness.NULLABLE)) {
-        return Nullness.NULLABLE;
-      }
-    }
-
-    Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
-    if (enclosingType == null) {
-      return Nullness.NONNULL;
-    }
-
-    return getGenericMethodParameterNullness(paramIndex, invokedMethodSymbol, enclosingType, state);
+    Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
+    Type invokedMethodType = invokedMethodSymbol.type;
+    if (enclosingType != null) {
+      invokedMethodType =
+          TypeSubstitutionUtils.memberType(
+              state.getTypes(), enclosingType, invokedMethodSymbol, config);
+    }
+    if (invokedMethodType instanceof Type.ForAll) {
+      Type.MethodType mt = invokedMethodType.asMethodType();
+      com.sun.tools.javac.util.List<Type> tvars = ((Type.ForAll) invokedMethodType).tvars;
+      java.util.List<Type> substitutedParamTypes =
+          substituteTypeArgsInGenericMethodType(tree, mt, tvars, state).getParameterTypes();
+      if (substitutedParamTypes != null
+          && getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam)
+              == Nullness.NULLABLE) {
+        return Nullness.NULLABLE;
+      }
+    }
+    return (enclosingType == null)
+        ? Nullness.NONNULL
+        : getGenericMethodParameterNullness(paramIndex, invokedMethodSymbol, enclosingType, state);
♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

847-861: Also handle explicit type args on generic constructors (NewClassTree).

The substitution now composes method-level args on top of class-level memberType — great. But the guard limits this to MethodInvocationTree, missing explicit type args on generic constructors (e.g., new C(...)). Extend the condition to include NewClassTree.

Apply:

-    if (tree instanceof MethodInvocationTree && invokedMethodType instanceof Type.ForAll) {
+    if ((tree instanceof MethodInvocationTree || tree instanceof NewClassTree)
+        && invokedMethodType instanceof Type.ForAll) {
       invokedMethodType =
           substituteTypeArgsInGenericMethodType(
               tree,
               invokedMethodType.asMethodType(),
               ((Type.ForAll) invokedMethodType).tvars,
               state);
     }
🧹 Nitpick comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

1069-1071: Use enum comparison directly.

Nullness is an enum; prefer == over Objects.equals for clarity and micro-efficiency.

Apply:

-          && Objects.equals(getTypeNullness(substitutedReturnType), Nullness.NULLABLE)) {
+          && getTypeNullness(substitutedReturnType) == Nullness.NULLABLE) {
-          && Objects.equals(
-              getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam),
-              Nullness.NULLABLE)) {
+          && getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam)
+              == Nullness.NULLABLE) {

Also applies to: 1175-1178

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6332d88 and 0c8bdd8.

📒 Files selected for processing (3)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (8 hunks)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (1 hunks)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java (0 hunks)
💤 Files with no reviewable changes (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java
🧰 Additional context used
🧠 Learnings (1)
📚 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/GenericMethodTests.java
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
  • TypeSubstitutionUtils (22-256)
⏰ 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). (8)
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
  • GitHub Check: Build spring-framework with snapshot
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
  • GitHub Check: Build caffeine with snapshot
🔇 Additional comments (3)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (2)

370-371: Good call: normalize trees before type lookup.

Stripping parentheses up front reduces edge cases in downstream logic that relies on exact Tree kinds.


1201-1228: Enclosing-type derivation looks right.

Covers implicit this/super (via enclosing ClassTree) and explicit receivers, and skips statics; aligns with the PR objective for super calls.

nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (1)

1086-1121: Great regression: exercises wildcard + nullable V in a Caffeine-like shape.

This captures the subtle substitution/identity case well and documents the current inference limitation with a TODO+SuppressWarnings.

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 (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (2)

1058-1075: Compose method-level type args on top of class-level substitution for return nullness

Return nullness is computed against the raw method type, discarding class-level substitutions when both class and method type variables are involved. Build from memberType(...) first, then apply explicit/inferred method type args.

-    // If generic method invocation
-    if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
-      // Substitute type arguments inside the return type
-      Type.ForAll forAllType = (Type.ForAll) invokedMethodSymbol.type;
-      Type substitutedReturnType =
-          substituteTypeArgsInGenericMethodType(tree, forAllType, state).getReturnType();
-      // If this condition evaluates to false, we fall through to the subsequent logic, to handle
-      // type variables declared on the enclosing class
-      if (substitutedReturnType != null
-          && Objects.equals(getTypeNullness(substitutedReturnType), Nullness.NULLABLE)) {
-        return Nullness.NULLABLE;
-      }
-    }
-
-    Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
-    if (enclosingType == null) {
-      return Nullness.NONNULL;
-    } else {
-      return getGenericMethodReturnTypeNullness(invokedMethodSymbol, enclosingType, state);
-    }
+    Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
+    Type invokedMethodType = invokedMethodSymbol.type;
+    if (enclosingType != null) {
+      invokedMethodType =
+          TypeSubstitutionUtils.memberType(
+              state.getTypes(), enclosingType, invokedMethodSymbol, config);
+    }
+    if (invokedMethodType instanceof Type.ForAll) {
+      Type substitutedReturnType =
+          substituteTypeArgsInGenericMethodType(tree, (Type.ForAll) invokedMethodType, state)
+              .getReturnType();
+      if (substitutedReturnType != null
+          && getTypeNullness(substitutedReturnType) == Nullness.NULLABLE) {
+        return Nullness.NULLABLE;
+      }
+    }
+    return (enclosingType == null)
+        ? Nullness.NONNULL
+        : getGenericMethodReturnTypeNullness(invokedMethodSymbol, enclosingType, state);

1161-1179: Compose method-level type args on top of class-level substitution for parameter nullness

Like return nullness, parameter nullness should start from memberType(...). Otherwise, explicit/inferred method type args can override class-level substitutions.

-    // If generic method invocation
-    if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
-      // Substitute the argument types within the MethodType
-      Type.ForAll forAllType = (Type.ForAll) invokedMethodSymbol.type;
-      List<Type> substitutedParamTypes =
-          substituteTypeArgsInGenericMethodType(tree, forAllType, state).getParameterTypes();
-      // If this condition evaluates to false, we fall through to the subsequent logic, to handle
-      // type variables declared on the enclosing class
-      if (substitutedParamTypes != null
-          && Objects.equals(
-              getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam),
-              Nullness.NULLABLE)) {
-        return Nullness.NULLABLE;
-      }
-    }
-
-    Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
+    Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
+    Type invokedMethodType = invokedMethodSymbol.type;
+    if (enclosingType != null) {
+      invokedMethodType =
+          TypeSubstitutionUtils.memberType(
+              state.getTypes(), enclosingType, invokedMethodSymbol, config);
+    }
+    if (invokedMethodType instanceof Type.ForAll) {
+      List<Type> substitutedParamTypes =
+          substituteTypeArgsInGenericMethodType(tree, (Type.ForAll) invokedMethodType, state)
+              .getParameterTypes();
+      if (substitutedParamTypes != null
+          && getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam)
+              == Nullness.NULLABLE) {
+        return Nullness.NULLABLE;
+      }
+    }
     if (enclosingType == null) {
       return Nullness.NONNULL;
     }
 
     return getGenericMethodParameterNullness(paramIndex, invokedMethodSymbol, enclosingType, state);
🧹 Nitpick comments (3)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (3)

1092-1105: Javadoc clarity: tree can be either method- or constructor-invocation

The method handles both MethodInvocationTree and NewClassTree; update the doc to match.

-   * @param tree the method invocation tree
-   * @param forAllType the generic method type
+   * @param tree the invocation tree (MethodInvocationTree or NewClassTree)
+   * @param forAllType the generic method type (after any class-level substitution if available)

1191-1218: Super/this receiver edge cases: fall back to enclosing class when receiver type is unavailable

For MemberSelectTree where the receiver is super/this, getTreeType(expr) can be null or surprising in some javac versions. Add a defensive fallback to the enclosing class type to keep behavior consistent with the implicit-this path.

       } else if (methodSelect instanceof MemberSelectTree) {
-        enclosingType = getTreeType(((MemberSelectTree) methodSelect).getExpression());
+        enclosingType = getTreeType(((MemberSelectTree) methodSelect).getExpression());
+        if (enclosingType == null) {
+          ClassTree enclosingClassTree =
+              ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class);
+          if (enclosingClassTree != null) {
+            enclosingType = castToNonNull(ASTHelpers.getType(enclosingClassTree));
+          }
+        }
       }

1212-1212: Use a single Verify style

You import both the class and the static method. Prefer one style (static import) for consistency.

-      Verify.verify(tree instanceof NewClassTree);
+      verify(tree instanceof NewClassTree);

Then remove the non-static import:

  • Remove import com.google.common.base.Verify;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0c8bdd8 and 51effa0.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
  • TypeSubstitutionUtils (22-256)
⏰ 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). (8)
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
  • GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
  • GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
  • GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
  • GitHub Check: Build caffeine with snapshot
  • GitHub Check: Build spring-framework with snapshot
🔇 Additional comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)

370-371: Parenthesis stripping in getTreeType — LGTM

This avoids missing annotations/types hidden by extra parentheses.

@msridhar msridhar requested review from lazaroclapp and yuxincs and removed request for yuxincs August 28, 2025 04:39
* @return Type of the tree with preserved annotations.
*/
private @Nullable Type getTreeType(Tree tree) {
tree = ASTHelpers.stripParentheses(tree);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Drive-by fix to handle parens

@msridhar
Copy link
Copy Markdown
Collaborator Author

@lazaroclapp I added your suggested tests in aa796f0 and they seem to work as I expect. The first passes (since it's passing a @NonNull Object to a @Nullable Object parameter), while the second fails (since it's passing a List<@NonNull Object> to a List<@Nullable Object> parameter).

@msridhar msridhar requested a review from lazaroclapp August 29, 2025 02:29
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: 0

🧹 Nitpick comments (2)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java (2)

2366-2385: Add a negative control to harden coverage

Consider adding a companion test where B extends A and calls super(null) (or super(o) with o annotated @nullable) to assert we still flag null passed to a non-null T.


2387-2408: Consider a symmetric method-call variant

Add an analogous superclass method test (A(List) m(...); B extends A<@nullable Object> calls super.m(l) with List) to mirror the constructor case and catch regressions in both paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 51effa0 and aa796f0.

📒 Files selected for processing (1)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
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.922Z
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.
📚 Learning: 2025-08-28T04:54:20.922Z
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.922Z
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
📚 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/GenericsTests.java
🔇 Additional comments (2)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java (2)

2366-2385: LGTM: super-ctor accepts NonNull for @nullable type arg

This positive test accurately exercises the new enclosing-type logic for super constructor calls and should pass under JSpecify mode.


2387-2408: LGTM: mismatched nested type arg is correctly rejected

The List vs List<@nullable Object> mismatch is the right regression guard for variance/inference subtleties on super-ctor calls.

Copy link
Copy Markdown
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

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

LGTM.

Presumably the Spring example needs a type like extends DefaultBodySpec<List<? extends @Nullable E super E>, correct? Which seems a bit convoluted, but it is in line with Java's treatment of collections vs arrays in terms of covariance, right? (Still swapping in some context here).

@msridhar
Copy link
Copy Markdown
Collaborator Author

FWIW this change makes Spring pass NullAway with this PR included:

msridhar/spring-framework@8fb857d

But it's quite possible it only passes due to other false negatives in NullAway (e.g., we don't yet do proper inference for the diamond operator).

@msridhar msridhar merged commit d7e9b24 into master Aug 30, 2025
12 of 14 checks passed
@msridhar msridhar deleted the fix-1246 branch August 30, 2025 19:02
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.

Sublcass<@Nullable Object> does not allow null super invocations

2 participants