-
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
Resolve regression for type annotations directly on inner types. #706
Conversation
Pull Request Test Coverage Report for Build #1025
💛 - Coveralls |
"import org.checkerframework.checker.nullness.qual.Nullable;", | ||
"class Test {", | ||
" Test.@Nullable Foo f1;", | ||
" @Nullable Test.Foo f2;", |
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.
Is it a problem that we treat these two as strictly equivalent for now? Feels like we need to, in order to support the annotationAppliedToInnerTypeImplicitly
case.
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 think we have to treat them equivalently for now. The former is a correct usage of the annotation, and the latter we must allow for backward compatibility. Do we know why Error Prone complains about the case below but not this one?
"import org.checkerframework.checker.nullness.qual.Nullable;", | ||
"class Test {", | ||
" Bar.@Nullable Foo f1;", | ||
" @Nullable Bar.Foo f2;", |
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.
Here we could potentially filter the first case, by only allowing inner types if the outer types match the containing scope, but it seems like extra brittleness and I am not sure supporting this is incorrect. Is there a difference in semantics here between the two cases above according to JSpecify or anything else?
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.
Actually, Error Prone does seem to care about this:
error: [NullableOnContainingClass] Type-use nullability annotations should annotate the inner class, not the outer class (e.g., write `A.@Nullable B` instead of `@Nullable A.B`).
Which is annoying, because many devs see the annotation as going on the field, not the type, whether or not it is a type use annotation. So they would write:
@Nullable
Bar.Foo f2;
and get an error from that checker.
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.
Still need to look at the whole PR, but yes, according to the spec, type-use annotations should go on the inner type:
https://jspecify.dev/docs/user-guide#type-use-annotation-syntax
We'll have to help developers adjust to this with good error messages.
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.
One possible concern
// We care about both annotations directly on the outer type and also those on directly | ||
// on an inner type, but wish to discard annotations on arrays, wildcards, or type arguments. | ||
return t.position.location.stream() | ||
.allMatch(l -> l.tag.equals(TypeAnnotationPosition.TypePathEntryKind.INNER_TYPE)); |
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 need allMatch
here? Don't we want to check that there exists an element of the list with tag INNER_TYPE
such that it is on the "innermost" type? E.g., what if we have field declaration A.@Nullable B.C foo;
; do we want to treat that as a @Nullable
reference?
Also we may have a type like A.@Nullable B<@Nullable C>
. I think we should treat this as as a @Nullable
reference, but allMatch
may be too strict? I'm not sure if we'll get a single TypeCompound
in two different locations for that case...
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.
Upon further investigation, I think Update: this is wrong. I think length of location
only has length greater than 1 for multi-dimensional arrays. For a type like A.@Nullable B<@Nullable C>
we will get two different TypeCompound
objects each of whose position.location
lists will be length 1.location
generally relates to level of nesting. Still investigating.
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 do need allMatch
, because A.B<@Nullable C>
shows as having the location array ["INNER_TYPE", "TYPE_ARGUMENT(0)"]
, so with anyMatch
, that one would match.
Think of it like a path: "annotation found at the type argument, position 0, of the inner type".
This is needed, because otherwise stuff like A<@Bar E>.B<C>
vs A<E>.B<@Bar C>
would be ambiguous if @Bar
just says ["TYPE_ARGUMENT(0)"]
.
So, in your example above, if we see A.@Nullable B<@Nullable C>
, what we get is two @Nullable
annotations (two calls to isDirectTypeUseAnnotation
) one which has the location array ["INNER_TYPE", "TYPE_ARGUMENT(0)"]
(the one by C
) and one which has location array ["INNER_TYPE"]
. We want to match only the second.
We want to match "a path including only a chain of inner types" and that's allMatch
.
"import org.checkerframework.checker.nullness.qual.Nullable;", | ||
"class Test {", | ||
" Test.@Nullable Foo f1;", | ||
" @Nullable Test.Foo f2;", |
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 think we have to treat them equivalently for now. The former is a correct usage of the annotation, and the latter we must allow for backward compatibility. Do we know why Error Prone complains about the case below but not this one?
1ad02f3
to
f300953
Compare
Oof, this is a bit worse than we thought, see the test in f300953. Right now we report an error for the Again for backward compatibility I think we want to treat both positions as making the field |
We specifically don't have JSpecify-like semantics for arrays yet, do we? I think this is an orthogonal concern to the purpose of this PR, but happy to have the test there if we currently handle the "correct" case. I think whenever we support the nullability of elements of arrays, that will be a major breaking change either way, since we always used the opposite semantics here from JSpecify. Edit: Actually, it very much looks like we don't handle the |
Let me try to summarize the overall situation here so we can think about potential solutions and their tradeoffs. #702 addressed an issue where writing Unfortunately, this change was bad for a couple of reasons:
For inner types, I think I'm fine with the solution currently implemented in this PR. It fixes point 1 above and gets rid of the surprising change in checking behavior for inner types from point 2. It does lead to a bit of weirdness, as shown in here: NullAway/nullaway/src/test/java/com/uber/nullaway/NullAwayCoreTests.java Lines 1113 to 1117 in 1b1a5b1
In particular, writing Dealing with arrays is a bit more complicated, since users may want to write annotations to express the nullability of array contents. If we adopt a solution like the one used in this PR for inner types, writing either We could also just stick with requiring users to write type use nullability annotations for arrays in the right spot. But that kind of breaking change seems weird to introduce by accident, and I'm not sure of other unintended consequences. That seems like a change we should introduce deliberately and possibly behind a flag at first to let users adopt gradually. So, I think our best option is to handle inner type and array type use annotations in the same way for now, as inner types are currently being handled in this PR. I'm not sure about a combination of these, e.g., Ok, those are all my thoughts for now. It's a messy situation but it's good that we are at least understanding the scope of the issue finally. |
Just as a note:
I don't think this is true (see the original test case). The change is only for arrays, because we are explicitly excluding arrays as a location, but it would be fine when annotating the outermost type, because that's just the location path
We could. This could be as easy as requiring that either all components of the location are inner types or all components of the location are arrays, but not a mix of both. There is an annoying edgecase where:
given explicitly does produce path But:
shows up as just Maybe the later is fine from the point of view of developer ergonomics? It's still a bit confusing, though... |
Yes I think you're right; my mistake.
Everything we do here is going to be confusing for a while 🙂 I think allowing for all inner types or all arrays is probably the best thing to do for now. |
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.
Mostly LGTM! Approved assuming minor comments below are addressed
"import org.checkerframework.checker.nullness.qual.Nullable;", | ||
"class A { class B { class C {} } }", | ||
"class Test {", | ||
" // At some point, we should not treat foo1, foo2, or foo4 as @Nullable.", |
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.
Just foo1
or foo2
? Looks like foo4
is now not treated as @Nullable
.
// proper deprecation of the incorrect behaviors for type use annotations when their | ||
// semantics don't match those of a declaration annotation in the same position. | ||
// See https://github.com/uber/NullAway/issues/708 | ||
boolean location_has_inner_types = 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.
Stick with standard lowerCamelCase?
boolean location_has_inner_types = false; | |
boolean locationHasInnerTypes = 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.
Ugh, sorry, the perils of alt-tabbing between Python and Java code 😅
// semantics don't match those of a declaration annotation in the same position. | ||
// See https://github.com/uber/NullAway/issues/708 | ||
boolean location_has_inner_types = false; | ||
boolean location_has_array = 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.
Similarly:
boolean location_has_array = false; | |
boolean locationHasArray = false; |
This resolves #705 by broadening our slightly overzealous filtering from #702.
We still want to filter type use annotations out when they happen on arrays, wildcards, or
type parameters. However, we can handle annotations on inner types (i.e.
Foo.@Nullable Bar
).More importantly, these inner types appear implicitly in natural code involving inner classes,
such as:
Not handling this case leads to confusing an unintuitive errors where a
@Nullable
annotationwith a type use location will seemingly stop working for certain fields at random, if those fields
are of an inner type.