sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Strict `void` lint request

Open lrhn opened this issue 3 years ago • 7 comments

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 yields void? as a part of the type.)

  • Never cast to void. No e as void (which isn't allowed grammatically, but again check after type alias expansion).

  • Never check for void. No e 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 type void. (Only static types count, not T foo<T>() => e which might get a type argument of void.)

  • Same in an async function with static future-return-type void.

  • return e; or => e in a non-async function with declared return type FutureOr<void> unless e has a static type which is a subtype of Future<void> (not allowing dynamic because there is no implicit downcast to Future<void>).

  • Assigning to a variable or other assignable expression with declared type void or void?.

  • Passing an argument to an optional parameter of type void or void?. (Can happen for, say, foo<T>({T? bar}) => ... with foo<void>().)

  • Passing any expression other than null as argument (or operator operand) to a required parameter of type void or void?.

  • Passing an argument expression to an optional parameter of type FutureOr<void> or FutureOr<void?> which does not have a static type which is a subtype of Future<void>.

  • Passing an argument expression to a required parameter of type FutureOr<void> or FutureOr<void?> which does not have a static type which is a subtype of Future<void>, or is the expression null.

  • Use an expression with static type void in 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> or FutureOr<void?> as operand of an is check or as cast with a type which is not a subtype of Future<void>.

    That is, not in a context with context type void. You need null there.

Which might be summarized as:

  • Treat an expression with static type void as 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 by as. Possibly already covered by void_checks.
  • Includes functions returning void, which should only and always use return; or body fall-through to return. Also somewhat covered by void_checks, but that allows => x as dynamic; or => x as Void; (with Void an alias for void). No arrow body exception here.
  • FutureOr<void> is special. You may assign a Future to it (any future), and you may check whether it is a Future, but never look at the non-future part.

lrhn avatar Jun 17 '22 11:06 lrhn

@lrhn : didn't this issue rather intend to be filed on https://github.com/dart-lang/linter ?

a14n avatar Jun 23 '22 21:06 a14n

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

bwilkerson avatar Jun 23 '22 21:06 bwilkerson

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!)

lrhn avatar Jun 24 '22 07:06 lrhn

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.

bwilkerson avatar Jun 24 '22 14:06 bwilkerson

Is this issue a duplicate (or a further development) of dart-lang/sdk#58053?

eernstg avatar Feb 01 '23 18:02 eernstg

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.)

rakudrama avatar Feb 01 '23 23:02 rakudrama

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.

bwilkerson avatar Jun 12 '25 18:06 bwilkerson

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.

FMorschel avatar Aug 22 '25 17:08 FMorschel