-
Notifications
You must be signed in to change notification settings - Fork 299
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
Conversation
Codecov ReportAttention: Patch coverage is
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. |
" if (this.target == null) {", | ||
" throw new IllegalArgumentException();", | ||
" }", | ||
" this.resolved = new MyMap<>();", |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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()));", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 Stream
s 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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To close the loop here I added a wiki page with some docs:
nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java
Outdated
Show resolved
Hide resolved
/** | ||
* 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; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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; | ||
}; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Preconditions.checkArgument( | ||
leafNode instanceof ClassTree || leafNode instanceof LambdaExpressionTree, | ||
"Unexpected leaf type: %s", | ||
leafNode.getClass()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Fixes #941
We propagate full nullability info from the enclosing context to callbacks passed to
Map.forEach
,Iterable.forEach
,List.removeIf
, and all methods onjava.util.stream.Stream