-
Notifications
You must be signed in to change notification settings - Fork 26
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
Expressing the "strictness" of enforcement #27
Comments
The basic idea seemed well received. |
We've discussed this internally in Kotlin team and have found an issue that would need to be clarified before we go down this road. The issue is orthogonality of
How this library's maintainer is supposed to annotate this method? Repeating analysis from #2, both of the simple options we have are bad:
Now, per this proposal, we give library maintainer a way out with
But what are the semantics differences between those two options? We've spend a while on the discussion and could not really find any difference either for tools or for library maintainer. We could not find a use-case to differentiate between the two either. Did we miss something? For a library maintainer, all they want to express while annotating their code is that "nullability contact of this method cannot be easily expressed". Basically they just need a single One possible resolution is to allow only Please, clarify. |
In this proposal The nullness annotations should be chosen purely based on the semantics of the nullness annotations. That is, if the method can return null, it should be annotated as The benefits over a
Please let me know whether this clarifies the proposal. |
The way it works with But what about |
Let's consider based the earlier example:
I think at a call site, |
This discussion might be a sign that the idea is flawed, and I'm really glad it was brought up. If we're lucky, we can look at the information conveyed by annotations on a method and we can neatly categorize them into "information that might result in more warnings" vs. "information that might lead to fewer warnings". (Note: for nullability, the kind of warnings we care most about are "this might throw NPE", not things like "seemingly unnecessary null check", which might complicate this model if we treat it as equally important.) Then we could simply say that @LessEnforced only affects the first kind of information and has no effect on the second. (The question of whether the annotation should even be allowed in a position where we can prove it has no effect should hopefully be a second-order concern.) I don't know if we will be as "lucky" as I just described or not. Maybe it's just fundamentally more complicated than that. Also I might already be missing some things you're trying to say (sorry!). |
In my mind, the major concerns that we have are not the details of errors/warning that our tools are going to produce, but the fact that we must give library writers a clear guidance of how they should decide between As a way out of this conundrum, here is a proposal. Can somebody add to this proposal a concrete and specific example of two different existing (non-hypothetical) methods in JDK, Guava or in another widely used Java library, where one of the methods would be best annotated with |
The guidance to library writers is something like: for each checker/type system/annotation, write the semantically correct signature. If that signature produces too many false positives for a checker, and these false positives are caused by some inherent complexity of that method, add If the false positives are only temporary, migration issues, the guidance is to instead add Specifically for nullness, there were many examples for As a concrete example for a Do note that errors depend on whether the
If we only think about nullness, having something like @LessEnforced
void foo() { ... } might not immediately make sense. @LessEnforced
@RequiresNonNull("this.field")
void foo() { ... } and this could tell a checker to not issue errors for a violation of this precondition and instead only issue a warning. Separating enforcement and migration issues from the semantic meaning of the checker-specific annotations seems like a big win. Enforcement and migration will come up with all systems that we define and having standard annotations for them will also be useful for third-party annotations. Does this make the motivation for separating We should discuss whether |
Let's assume that the goal is to recommend the following annotations:
Let us see how it affects the users and implementors of these methods. It seems that they produce the same effect of suppressing all the nullability warnings on use-site of Franky, the analogy with On the other hand, Let me add, that I'm not questioning here the proposal for |
Both Implementors of a method would still see the usual errors/warnings and should use Tools are free to interpret A developer usually has a choice whether they want to use For either Regarding implementors, I would also like to discuss method overriding. I would like to argue that class A {
@LessEnforced
@Nullable Object foo() ...
}
class B extends A {
@Override
@Nullable Object foo() ...
}
A a = new B();
B b = new B();
a.foo().toString(); // less strict enforcement
b.foo().toString(); // strict enforcement Do people agree with this handling of overrides? |
Quickly on the override question: I think it is a general principle that we "never" want to automatically inherit annotation-based knowledge from a supermethod, because it is not reasonable to expect developers who are seeking understanding to have to chase upward an arbitrary number of levels. There will be some annotations where we need the policy that any overrides are required to repeat the same annotation (or stronger). I need to open a lot of issues on topics like this. |
I want to propose to move away from
So I'd propose to abandon |
These are of course valid points. I think that this is not exactly a decision that we can make ("drop @LessEnforced"); what we can do instead is
OR, another approach is to sidestep the whole debate by saying we'll kick the relevant requirements down the road six months and just deal with them then. There are probably some subset of the requirements we can safely do that with. |
Oh, er... after just having said these are valid points now I want to quibble a bit with them, sorry. The previous comment is the more important branch of the conversation.
Next I'm curious about "can express the desired semantics (platform/flexible type)". My understanding of a "platform type" is that it's what you're forced to use when you have absolutely zero knowledge to go on from the API designer. I see that as something we would never in any circumstances want the API owner to choose intentionally; that would only illustrate that we had failed to properly account for their use case. I think they should always have a way to get what they want that makes their code distinguishable from "no one thought about this yet". This is discussed under what is currently requirement #11 (look for "part-legacy"). |
I'm honestly confused. My understanding is that this issue was filed as a proposed solution to issues around (As an aside let me say I've been assuming these cases are rare. That is, for most method parameters and results, All of this is specific to nullness; I don't see an argument to be made that it's orthogonal. It also doesn't have anything to do with legacy or non-legacy, in my opinion. These are (rare) cases where it is sensible for API owners to want Kotlin to use platform types. And I agree that we should strive to give API owners the ability to express this intent. But I have concerns about providing this expressiveness with an annotation that is intended to apply to non-nullness annotations as well. Add to that the combinatorial problem that, from Kotlin's perspective at least, One other issue I ran into when experimenting with this was that since
As an API designer I might only want a platform type for Finally, again, I'm not proposing to give up on this use case; I'm proposing to solve it in a nullness-specific way instead, e.g.
Where |
Ugh, I'm sorry for misunderstanding so badly. Now I see what you mean. I have to come back to this later but my gut reaction is I will probably agree with you by the time I process it all. |
For now, we are fine with this being addressed via @NullnessUnknown, so this is considered resolved. [cpovirk edit 2021-03-09: There's definitely not consensus at this point that we should endorse/recommend |
Google-side discussions of #2 lead us to the proposal below.
I wanted to write up my understanding of the discussions, to give people a chance to think about this before the meeting on Tuesday.
@kevinb9n please clarify or start with a different issue.
Basic idea
Instead of adding additional nullness annotations or adding attributes to the nullness annotations, we add additional declaration annotations that express the "strictness" of enforcement.
The default is that annotation semantics should be enforced by tools.
Specifically-marked APIs have alternative, more relaxed enforcement, without specifying exactly how that relaxed handling is done.
A conservative tool could just ignore this additional information.
Another tool could turn related errors into warnings.
Yet another tool could use runtime checks for such methods.
The particular checker (type)-annotations always have their precise semantics.
For example, a
@Nullable
return type means that the method can sometimes returnnull
.The additional declaration annotation conveys how convenient enforcing the property would be.
For example, if the method is usually expected to be non-null, a tool could chose to not enforce null checks.
Naming
We need to discuss what qualifier names would best express this intent. Some ideas:
@MoreEnforced
/@LessEnforced
@StrictChecks
/@LenientChecks
@PreventMisuse
/@AllowMisuse
Scoping
As the less-enforced status should be used sparingly, we could restrict its usage to method declarations.
We would then also only need one annotation, where the implicit alternative is strict enforcement.
Alternatively, we could allow the annotation on the package/class/method level to allow specifying on all levels of granularity. We would then probably want two annotations to allow to specify possible nestings, e.g. the package is marked as less-enforced, however a class within it is strictly-enforced.
Relation to Migration
#26 proposes an
@UnderMigration
annotation. We feel that these express two separate dimensions.Migration is time-bounded, whereas enforcement is about the particular API and will not change.
Strict enforcement and additional checker information
One thing to note: even for a strictly-enforced
@Nullable
annotation, a checker might have some additional information that tells it that in this particular use something is non-null. So strict enforcement doesn’t mean every@Nullable
dereference will give an error.For example, a checker could support pre-/post-condition annotations on a method that gives additional information in certain invocations.
Selecting which checks should be less enforced.
A blanket
@LessEnforced
is probably too coarse. Similar to@UnderMigration
we need a way to express which checks are less enforced.would express that only the nullness part of the specification is less strict and the
@CRV
should still be enforced.Like for
@UnderMigration
we need to discuss what the best representation for this is, e.g. class literals as above, canonical checker names, names that are also used for warning suppressions, etc.Example
#2 contains many examples, all of which should be able to use this proposal. Let's look at
findViewByID
:In the use:
Errors on uses of
button
would be inconvenient on users.We would declare the method as:
The return type is still correctly annotated as
@Nullable
. However, the@LessEnforced
tells clients of this API to be less strict about enforcing the rules, which in this case could mean to treat the method as returning a@NonNull
value.An analysis is still free to give better information, e.g. by checking proper resource IDs and warning on unknown values.
The text was updated successfully, but these errors were encountered: