Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propagate more nullability info to lambdas known to be invoked synchronously #952

Merged
merged 18 commits into from Apr 30, 2024

Conversation

msridhar
Copy link
Collaborator

@msridhar msridhar commented Apr 24, 2024

Fixes #941

We propagate full nullability info from the enclosing context to callbacks passed to Map.forEach, Iterable.forEach, List.removeIf, and all methods on java.util.stream.Stream

Copy link

codecov bot commented Apr 24, 2024

Codecov Report

Attention: Patch coverage is 77.14286% with 16 lines in your changes are missing coverage. Please review.

Project coverage is 86.06%. Comparing base (719b167) to head (b8b8078).

Files Patch % Lines
.../nullaway/handlers/SynchronousCallbackHandler.java 83.33% 1 Missing and 6 partials ⚠️
...er/nullaway/handlers/OptionalEmptinessHandler.java 14.28% 6 Missing ⚠️
...a/com/uber/nullaway/handlers/CompositeHandler.java 77.77% 1 Missing and 1 partial ⚠️
...m/uber/nullaway/handlers/AccessPathPredicates.java 66.66% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #952      +/-   ##
============================================
- Coverage     86.08%   86.06%   -0.02%     
- Complexity     2018     2032      +14     
============================================
  Files            79       81       +2     
  Lines          6612     6669      +57     
  Branches       1280     1294      +14     
============================================
+ Hits           5692     5740      +48     
- Misses          510      516       +6     
- Partials        410      413       +3     

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

@msridhar msridhar marked this pull request as ready for review April 25, 2024 23:13
@msridhar msridhar changed the title [WIP] Propagate more nullability info to lambdas known to be invoked synchronously Propagate more nullability info to lambdas known to be invoked synchronously Apr 25, 2024
" if (this.target == null) {",
" throw new IllegalArgumentException();",
" }",
" this.resolved = new MyMap<>();",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe use a type other than MyMap for this.resolved here? Got a bit confused think that was what changed the test case, but it doesn't, right? What matters is that target is MyMap.

Copy link
Collaborator Author

@msridhar msridhar Apr 28, 2024

Choose a reason for hiding this comment

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

See 8a03676

" List<Object> l = new ArrayList<>();",
" l.forEach(v -> System.out.println(v + this.f.toString()));",
" Iterable<Object> l2 = l;",
" l2.forEach(v -> System.out.println(v + this.f.toString()));",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Worth adding a case where we set this.f = null then do one of these and show it now reports an error?

Copy link
Collaborator Author

@msridhar msridhar Apr 28, 2024

Choose a reason for hiding this comment

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

See 26bc3a0

@@ -191,7 +191,8 @@ private Stream<T> test1(Stream<T> stream) {

private Stream<T> test2(Stream<T> stream) {
Preconditions.checkNotNull(ref);
// BUG: Diagnostic contains: dereferenced expression ref is @Nullable
// no error since we propagate nullability facts to stream callbacks, which
// in sane code are invoked soon after the stream is created
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess I go a bit back and forth between "this is probably safe" and "restricting it to cases where the stream doesn't escape the method will remove most false positives too and be safer". Your call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let me think through a bit more what it would take to implement the safer version.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@lazaroclapp I thought about this more and I think a reasonably precise safer version would be kind of a pain to implement. My thought was to try to detect if the overall chain of stream calls ended in a terminal operation. But there are a bunch of terminal operations on Stream itself, and then there are methods to convert Stream to IntStream, DoubleStream, etc., each with their own terminal operations. (As an aside, in a follow-up we should probably handle those primitive stream types in the same way that we handle Streams themselves.) We could also just check if the enclosing method has a return type of Stream, but that is imprecise, and also misses cases like passing as a parameter or storing in a field.

Bottom line, my feeling is at this point, we are most likely ok going with the less safe version. But, I think it's time we add a wiki page on our stream handling in general, documenting how we propagate nullability facts from filter() methods to subsequent callbacks and also this new propagation of nullability facts from the enclosing context. On that page we could give caveats on cases where our handling could be unsound, and why we decided to do what we did.

Let me know what you think.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Of course, what this really needs is a new type system about the state of Stream objects... but in the absence of that, just eagerly propagating nullability of APs in the closure is probably safe enough (kidding about the Stream type-state system... mostly)

Comment on lines 257 to 267
/**
* An AccessPath predicate that always returns false. Used for optimizing
* getAccessPathPredicateForNestedMethod.
*/
static final Predicate<AccessPath> FALSE_AP_PREDICATE = ap -> false;

/**
* An AccessPath predicate that always returns true. Used for optimizing
* getAccessPathPredicateForNestedMethod.
*/
static final Predicate<AccessPath> TRUE_AP_PREDICATE = ap -> true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why part of CompositeHandler vs Handler? Also, maybe we want Handler.AccessPathPredicates.[TRUE|FALSE]_AP_PREDICATE? (We can always static import to avoid repetition)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See my comment above, I'm happy with making this change if you think it's better

}
return false;
return false;
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unrelated to this PR, but... are we missing unit tests for this handler? Or is Codecov wrong?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We are in fact missing coverage! It turns out that #678 made the pseudo-field simulating contents of an Optional final, which means we already (safely) propagate nullability facts about that pseudo-field into lambdas, which means we no longer need this handler method! I'll go ahead and delete this method in a separate PR. Nice to have the test coverage exposed!

*/
boolean includeApInfoInSavedContext(AccessPath accessPath, VisitorState state);
Predicate<AccessPath> getAccessPathPredicateForNestedMethod(TreePath path, VisitorState state);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting that our existing stream handling support doesn't really use this Handler method, it seems... I assume because of how handlers work that we just end up composing the information about x we get from the stream handler when looking at s.filter(x -> ...).map(x -> ...) with the info this gives us about the APs from the closure?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that's exactly right. This handler method is used to decide which access paths from the enclosure should be maintained when analyzing a lambda, beyond the standard ones (paths involving effectively-final locals). Before I think we only used this for the OptionalEmptinessHandler

Comment on lines +53 to +56
Preconditions.checkArgument(
leafNode instanceof ClassTree || leafNode instanceof LambdaExpressionTree,
"Unexpected leaf type: %s",
leafNode.getClass());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we know the leafNode is a ClassTree or LambdaExpressionTree here? (I assume at some point we are filtering out method references and the like?

Edit: Guess it has to do with where we call updateEnvironmentMapping. But by the time we get to getNullnessInfoBeforeNewContext that's no longer very clear in our docs/checks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, tweaked the docs and method name in 34bb9f3

@msridhar msridhar merged commit edcfe49 into uber:master Apr 30, 2024
10 of 12 checks passed
@msridhar msridhar deleted the issue-941 branch April 30, 2024 03:03
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.

Variable checked non-null outside a forEach or Stream lambda triggers an error
2 participants