sdk
sdk copied to clipboard
Strict `void` lint request
A void expression should be treated as not having a value. The language is permissive for backwards compatibility reasons, and allow you to assign void to void and dynamic, and assign void, dynamic and null to void.
I'd like to restrict doing so as much as possible. With a strict_void lint (or possible multiple separate lints, if that makes more sense), it would be an error to:
-
Never write the type
void?. (You can't directly, but can through a type alias expansion. It's an error if type alias expansion yieldsvoid?as a part of the type.) -
Never cast to
void. Noe as void(which isn't allowed grammatically, but again check after type alias expansion). -
Never check for
void. Noe is void(after alias expansion). -
Use
return e;in a function body in, or use=> e;as a function body, of a function declaration with declared return typevoid. (Only static types count, notT foo<T>() => ewhich might get a type argument ofvoid.) -
Same in an
asyncfunction with static future-return-typevoid. -
return e;or=> ein a non-asyncfunction with declared return typeFutureOr<void>unlessehas a static type which is a subtype ofFuture<void>(not allowingdynamicbecause there is no implicit downcast toFuture<void>). -
Assigning to a variable or other assignable expression with declared type
voidorvoid?. -
Passing an argument to an optional parameter of type
voidorvoid?. (Can happen for, say,foo<T>({T? bar}) => ...withfoo<void>().) -
Passing any expression other than
nullas argument (or operator operand) to a required parameter of typevoidorvoid?. -
Passing an argument expression to an optional parameter of type
FutureOr<void>orFutureOr<void?>which does not have a static type which is a subtype ofFuture<void>. -
Passing an argument expression to a required parameter of type
FutureOr<void>orFutureOr<void?>which does not have a static type which is a subtype ofFuture<void>, or is the expressionnull. -
Use an expression with static type
voidin any place where it's value is used. That's probably anywhere but:- The expression of an expression statement.
- As an initializer expression of a
for(*here*;...;...)statement. (When it's not a declaration). - As an increment expression of a
for(...;...;*here*, *or here*, ...)statement.
-
Use an expression of type
FutureOr<void>orFutureOr<void?>as operand of anischeck orascast with a type which is not a subtype ofFuture<void>.That is, not in a context with context type
void. You neednullthere.
Which might be summarized as:
- Treat an expression with static type
voidas not having a value. Only use it in contexts where a value is immediately discarded. No casting, no assigning to dynamic, no checking its runtime type. There is no value. We currently allow casts and type checks to any type. - Likewise, don't treat an actual value as being compatible with
null. No casting to, implicitly by upcast to context type or explicitly byas. Possibly already covered byvoid_checks. - Includes functions returning
void, which should only and always usereturn;or body fall-through to return. Also somewhat covered byvoid_checks, but that allows=> x as dynamic;or=> x as Void;(withVoidan alias forvoid). No arrow body exception here. FutureOr<void>is special. You may assign aFutureto it (any future), and you may check whether it is aFuture, but never look at the non-future part.
@lrhn : didn't this issue rather intend to be filed on https://github.com/dart-lang/linter ?
I don't have an objection to this being implemented as a lint, but it might make more sense for it to be implemented as a language option, along the same lines as strict-casts, strict-inference, and strict-raw-types, possibly called strict-void.
@leafpetersen @srawlins
I'm not sure what a "language option" really is. Couldn't it just be a named set of lints? I don't see any need for more categories of user-enabled/configured source analyses.
@a14n Absolutely should be in linter repo. My bad. (So that's why I couldn't find it!)
I'm not sure what a "language option" really is.
As I understand it, the notion of a language option was originally designed to be a way for users to further tighten the semantics of the language. It was motivated by a desire to clean up the strong_mode option that we had before we had introduced the notion of experiments.
In the analysis options file it looks something like the following:
analyzer:
language:
strict-casts: true
Couldn't it just be a named set of lints?
Yes, I believe that we could replace each of the current language options with a warning that is disabled by default. Whether there's a compelling reason to support a different way of enabling these warnings, especially in light of the proposed simplification of errors and warnings, is definitely something we should consider. I hadn't really thought about that question, I was merely pointing out that this is very similar to what we now call language options.
Is this issue a duplicate (or a further development) of dart-lang/sdk#58053?
I think this goes too far and throws out some good things with the bad. Chaining explicit-void-to-explicit-void is a nice language feature.
Writing methods that delegate with => without worrying about the 'type' is a nice affordance, allowing the delegating pattern be shorter and have visual uniformity between void and non-void methods. It is make-work, i.e. an impediment to coding velocity, to have to flip between a method definition that has an expression body vs a full body, and having to avoid => is especially ugly in closures used as arguments.
The current rules allow annotations of expression-statements by annotating a variable.
@pragma('dart2js:disable-inlining')
void _ = print('hello');
If the above becomes illegal then we will need another way of annotating an expression-statement. (The pattern is not yet in common usage but dart2js is heading towards finely scoped annotations to restrict the scope of non-spec behaviour (https://github.com/dart-lang/sdk/issues/49475). As the different annotations become more finely scoped, using this pattern would become the recommended practice.)
I'm wondering whether the proposed solution is looking at the problem backward. (Probably not, but have to ask.)
Is there any value in using void as (a) the type of a local variable, parameter, field, or top-level variable or (b) as the return type of a getter? I could imagine a use case for using it as the type of a parameter if the method overrides another method, though I'd try to find a different solution for that case.
Maybe we should reduce the number of places that the cases above can occur rather than, or in addition to, flagging those cases.
Is there any value in using void as [...] (b) as the return type of a getter?
I think only if you had a FutureOr<void> case, and in a subclass, you want to be sure it is synchronous, but still weird IMO.
I think tagging all of those cases you suggested is a good idea.