Skip to content

Improve inference for generic method with void-returning lambda argument#1312

Merged
msridhar merged 11 commits intouber:masterfrom
dhruv-agr:issue-1294-v2
Oct 19, 2025
Merged

Improve inference for generic method with void-returning lambda argument#1312
msridhar merged 11 commits intouber:masterfrom
dhruv-agr:issue-1294-v2

Conversation

@dhruv-agr
Copy link
Copy Markdown
Contributor

@dhruv-agr dhruv-agr commented Oct 16, 2025

javac inference for lambda argument leads to incorrect subtype constraint generation.
solution: Skipping subtype constraint generation for lambda argument and using the results from constraint solver inference by saving it in a new map.

This currently only handles void-returning lambdas. We will extend inference to handle return statements within lambdas in a follow-up.

Fixes #1294

Summary by CodeRabbit

  • New Features
    • Improved null-safety analysis for lambda arguments in generic contexts, reducing false nullability and dereference warnings inside lambdas.
  • Tests
    • Added a test covering lambda-argument type inference and related nullability/dereference diagnostics.
  • Chores
    • Adjusted JDK-related test wiring and increased CI build verbosity (--info --stacktrace) for clearer failure traces.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

AccessPathNullnessPropagation now constructs CoreNullnessStoreInitializer with analysis.getGenericsChecks(). CoreNullnessStoreInitializer stores that GenericsChecks instance; its constructor changed and its former static lambdaInitialStore is now an instance method that consults genericsChecks.getInferredLambdaType(...) to resolve lambda types when computing member/parameter substitutions. GenericsChecks gained an inferredLambdaTypes cache, a public getter, logic to populate it during generic-method inference, and cache-clear behavior. A new test issue1294_lambdaArguments was added to exercise lambda-argument inference.

Possibly related PRs

Suggested reviewers

  • yuxincs
  • lazaroclapp

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning While the core changes in GenericsChecks, CoreNullnessStoreInitializer, AccessPathNullnessAnalysis, and GenericMethodTests are clearly in scope for improving lambda argument type inference, two changes appear to be out of scope: modifications to jar-infer/jar-infer-lib/build.gradle (adding testJdk25 toolchain configuration and bundleReleaseAar dependency) and .github/workflows/continuous-integration.yml (upgrading action versions and adding verbose flags). These build and CI infrastructure changes are not related to the stated objective of fixing type inference for generic methods with lambda arguments and lack clear justification in the PR context.
✅ 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 "Improve inference for generic method with void-returning lambda argument" accurately summarizes the main change across the core files (AccessPathNullnessAnalysis, CoreNullnessStoreInitializer, and GenericsChecks), which all work together to improve type inference for generic methods when lambda arguments are passed. The title is clear, concise, and specific enough for reviewers to quickly understand the primary objective without being overly verbose. It directly reflects the linked issue #1294 and the PR's stated purpose.
Linked Issues Check ✅ Passed The code changes directly address the objectives from linked issue #1294. The modifications to GenericsChecks add an inferredLambdaTypes cache and a public getter to store and retrieve inferred lambda types. CoreNullnessStoreInitializer now accepts a GenericsChecks instance and uses it in lambdaInitialStore to override AST lambda types with inferred types from the constraint solver. AccessPathNullnessAnalysis is updated to pass GenericsChecks to the CoreNullnessStoreInitializer. These changes enable correct type inference for generic method calls with lambda arguments while handling void-returning lambdas as specified. A test is also added (issue1294_lambdaArguments) to validate the feature.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3de052 and e623766.

📒 Files selected for processing (1)
  • jar-infer/jar-infer-lib/build.gradle (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • jar-infer/jar-infer-lib/build.gradle

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 October 16, 2025 19:33
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/main/java/com/uber/nullaway/generics/GenericsChecks.java (2)

105-107: Add documentation for the public getter.

The getInferredLambdaType method is part of the public API but lacks documentation. Consider adding a Javadoc comment explaining:

  • When this method returns a non-null value (i.e., for lambda arguments in generic method calls that have undergone inference)
  • When it returns null (i.e., for lambdas not part of generic method inference, or before inference has been performed)
  • The intended callers and usage context

Example:

+  /**
+   * Returns the inferred type for a lambda expression that was passed as an argument to a generic
+   * method call. The type is computed during constraint-based inference for generic method calls
+   * and includes inferred nullability for type parameters.
+   *
+   * @param tree the lambda expression tree
+   * @return the inferred functional interface type with nullability annotations, or null if the
+   *     lambda was not part of a generic method call requiring inference
+   */
   public @Nullable Type getInferredLambdaType(LambdaExpressionTree tree) {

983-992: Consider adding a comment to explain lambda fallback behavior.

The code correctly uses cached inferred types for lambda arguments, but the fallback behavior when a lambda is not in the cache (i.e., actualParameterType remains null and checks are skipped at line 993) is subtle. This is actually correct because:

  1. getTreeType would return the functional interface type, which isn't useful for checking type parameter nullability
  2. Only generic method calls populate the cache through inference
  3. For non-generic methods, there are no type parameters to infer/check anyway

However, this reasoning is not obvious from the code. Consider adding a comment explaining why lambdas not in the cache are skipped rather than falling back to getTreeType.

   Type actualParameterType = null;
   if ((currentActualParam instanceof LambdaExpressionTree)) {
+    // For lambda arguments, use the inferred type from the constraint solver if available.
+    // If not in cache (e.g., non-generic method calls), actualParameterType remains null
+    // and checks are skipped below, which is correct since getTreeType() would only return
+    // the functional interface type, not the lambda's actual return type.
     Type lambdaInferredType = inferredLambdaTypes.get(currentActualParam);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between def40ba and e9a40e9.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 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.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.
📚 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/generics/GenericsChecks.java
🔇 Additional comments (3)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (3)

627-636: Good approach to cache inferred lambda types.

The logic correctly captures the constraint solver's inference results for lambda arguments, storing the formal parameter type with inferred nullability annotations. This enables downstream code to use these types instead of relying on potentially incorrect javac inference.


738-746: LGTM - correctly skips subtype constraints for lambdas.

This change addresses the core issue described in the PR. By skipping subtype constraint generation for lambda arguments (which can be incorrect due to javac inference), and instead relying on the constraint solver's inference results (stored in lines 627-636), the implementation avoids the incorrect constraint generation problem.


1686-1686: LGTM - proper cache cleanup.

The lambda types cache is correctly cleared along with the existing inference cache to prevent memory leaks between compilation units.

Copy link
Copy Markdown
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.

This is looking pretty clean! Just a few minor comments

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (1)

1249-1274: Good coverage for lambda-argument inference; matches prior ask.

ret3/ret4 were added and ret2 asserts the deref error as intended. Looks solid.

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

988-997: Fallback to AST type when no inferred lambda type available.

Prevents silently skipping param checks for non-generic or failed-inference cases.

Apply:

-              Type actualParameterType = null;
-              if ((currentActualParam instanceof LambdaExpressionTree)) {
-                Type lambdaInferredType = inferredLambdaTypes.get(currentActualParam);
-                if (lambdaInferredType != null) {
-                  actualParameterType = lambdaInferredType;
-                }
-              } else {
-                actualParameterType = getTreeType(currentActualParam, state);
-              }
+              Type actualParameterType;
+              if (currentActualParam instanceof LambdaExpressionTree) {
+                Type lambdaInferredType = inferredLambdaTypes.get(currentActualParam);
+                actualParameterType =
+                    (lambdaInferredType != null)
+                        ? lambdaInferredType
+                        : getTreeType(currentActualParam, state);
+              } else {
+                actualParameterType = getTreeType(currentActualParam, state);
+              }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9a40e9 and a5a29c7.

📒 Files selected for processing (3)
  • nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java (4 hunks)
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (6 hunks)
  • nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 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.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.
📚 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/generics/GenericsChecks.java
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
  • GenericsChecks (67-1723)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
  • TypeSubstitutionUtils (22-287)
🔇 Additional comments (5)
nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java (1)

114-123: Nice: documents EP traversal ordering for lambda inference.

This resolves the subtle “have we inferred yet?” concern without extra plumbing.

nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (4)

102-111: Cache for inferred lambda types looks good.

Docstring is clear and API is minimal.


630-640: Recording inferred types for lambda args at solve-time is the right place.

Keeps inference concerns localized; no extra passes needed.


741-744: Correct to skip subtype constraints for lambda arguments.

Prevents javac’s immediate target typing from polluting our constraints; matches the PR goal.


1690-1691: Clearing lambda-inference cache with the other maps avoids leaks.

Good cleanup.

Copy link
Copy Markdown
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.

LGTM!

@msridhar msridhar changed the title issue 1294 generic method with lambda argument Improve inference for generic method with void-returning lambda argument Oct 18, 2025
@msridhar msridhar enabled auto-merge (squash) October 18, 2025 17:28
@msridhar msridhar disabled auto-merge October 18, 2025 18:01
@codecov
Copy link
Copy Markdown

codecov bot commented Oct 18, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.42%. Comparing base (d8f298d) to head (4babb6d).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1312      +/-   ##
============================================
+ Coverage     88.39%   88.42%   +0.03%     
- Complexity     2516     2523       +7     
============================================
  Files            94       94              
  Lines          8393     8415      +22     
  Branches       1657     1661       +4     
============================================
+ Hits           7419     7441      +22     
  Misses          491      491              
  Partials        483      483              

☔ 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 enabled auto-merge (squash) October 19, 2025 16:59
@msridhar msridhar merged commit 0a2e2b0 into uber:master Oct 19, 2025
10 checks passed
msridhar added a commit that referenced this pull request Nov 13, 2025
… return statements (#1337)

This is a follow up to #1312 
While generating constraints for params in
`generateConstraintsForParam`, if argument is a lambda expression, then
generate constraints for the return expression inside lambda and the
functional interface method return type. If the return expression is a
method invocation then recursively call `generateConstraintsForCall`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved null-safety checks for lambdas used with generic methods in
JSpecify mode by using more accurate inferred lambda return types,
reducing false diagnostics about returning nullable values.

* **Tests**
* Expanded and re-enabled tests to validate lambda return-type inference
with generic method calls and updated expectations to reflect corrected
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Manu Sridharan <msridhar@gmail.com>
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: inference for calls to generic methods not working for lambda arguments

2 participants