-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
proposal: avoid_futureor_void
#59232
Comments
We should change Then It would be the dual of (Also wouldn't allow overriding a |
That sounds like a severely breaking change. Of course, it would bring up the old issue that, say, a This means that we may accidentally consider a consistently-null source (intended to be a non-source because it has the type argument Iterable<Y> f<X extends Y, Y>(Iterable<X> iter) => iter;
void main() {
var dontLook = <void>[null]; // Or a thousand nulls.
Iterable<int?> maybeInts = f(dontLook);
for (int? i in maybeInts) {
... // workworkwork. But we shouldn't work on `dontLook` in the first place!
}
dontLook.add(1); // OK, adds another null.
(dontLook as dynamic).add(1); // Throws? Checks the actual type argument and changes 1 to null?
} However, it might then be simpler to change the usage, and say that In any case, the proposal to lint against using |
No easy fixes 😥 Anyway, my ramblings aside, I think this is a good warning to give. While you can write I'd be fine with allowing |
I support this. |
Here is one way to make progress on this issue: https://dart-review.googlesource.com/c/sdk/+/382403. |
I support this. |
Coming back to this, I still see reasonable uses of I mostly use it in a contravariant position, allowing clients of my API to pass one of It's not the same type as Having something like var ints = <int>{};
var stringLengths = strings.map((s) => s.length);
doEach(stringLengths, ints.add); because the return type of I can see the reasoning here, but I think there is enough reasonable uses for type If the lint can allow the type in contravariant positions, and if working with the result of something that comes from such a contravariant position, then I think it's fine. It's introducing new values of that type that is questionable. Don't declare a function returning A |
Makes sense! I'll adjust the lint. |
@lrhn, thinking about this one more time I'm still not convinced. Let's consider the case where What do we gain by using a parameter type like The answer might be that it sends an informal signal to the client who is going to pass an actual argument to this parameter. // Tell the caller that we will treat `fun` in a certain way.
void doTask(FutureOr<void> Function() fun) {...} The signal cannot be "when we call your function we will ignore the result", because that's much better modeled by giving The signal could be "if you provide a function that declares that it returns a future then we will await it and discard the result of awaiting, otherwise we will discard the result". This matches the meaning of a hypothetical union type like The signal could be "when we call your function and it returns a result, any The crucial part is that this is a dynamic criterion, ignoring the statements of intent that the given actual argument may provide by means of its static type. Here is an example why it is possible, and not unlikely, that a future may be returned in a situation where it should not be awaited: import 'dart:async';
// We need to log a few lines, but it doesn't have to be right now.
void logSomethingButDoNotWait() =>
Future<void>.delayed(Duration(seconds: 5), () => print('Logging!'));
void work() => print('Work!');
Future<void> doTasks(List<FutureOr<void> Function()> tasks) async {
for (var task in tasks) {
var result = task();
if (result is Future<void>) await result;
}
}
void main() {
doTasks([work, work, logSomethingButDoNotWait, work, work]);
} Running it (e.g., in DartPad), we can see that it waits for a long time, for something that we shouldn't await in the first place. Basically, we are ignoring that To me it looks like a step backwards to ignore the static types and return to a dynamic test, in order to decide whether the result returned by a callback should be awaited or not, especially because there's no way we can make that decision unambiguously at run time. In the example, we're blocked for a while, despite the stated intention for the function You could say that we shouldn't write functions like void logSomethingButDoNotWait() async {
await Future<void>.delayed(Duration(seconds: 5));
// ... lots of hard work that doesn't have to happen right now.
print('Logging!');
} In both cases the returned object is a future, in both cases it is explicitly indicated using types that it should not be awaited, and in both cases the dynamic test on the returned object would ignore this intention. (Compare: With I do recognize that we can't pass a function of type For local documentation, we could use a type alias: // This can be any object whatsoever, but a `Future<void>` will get a special treatment.
typedef FutureVoidOrAny = Object?; I think it's more reasonable to use Of course, we still have the error prone behavior of relying on a dynamic check (and ignoring that some futures are delivered with a type that says "even though you can, you should not await this!"). For that, I'd recommend that we use a test on the dynamic type of the function rather than on the returned result: /// ... document that a `Future<void> Function()` is given special treatment ...
Future<void> doTasks(List<Object? Function()> tasks) async {
for (var task in tasks) {
if (task is Future<void> Function()) {
await task();
} else {
task();
}
}
} With this version of |
The signal is indeed that the function can either return a future whose value will be ignored, or a non-future value which will then be ignored. I'd be happy to use
It's a union type. Static union types are always distinguished dynamically, it's how you use them. So, yes.
That should never be the case. I don't see a big difference between the last example and: /// ... document that a `Future<void> Function()` is given special treatment ...
Future<void> doTasks(List<FutureOr<void> Function()> tasks) async {
for (var task in tasks) {
if (task() case Future<void> future) await future;
}
} other than it being more complicated, and that it risks mishandling a function with actual return type |
Union types can be discriminated by a run-time test if the distinction has a run-time representation. For instance, With almost any other type However, when This kind of discrimination is weakly typed (I used the word 'dynamic', but the point I'm making is that it is weakly typed, not that it happens at run time), because there's no way the provider of the given value can use static typing (like return types on functions or declared types of variables) to influence the chosen branch. It's the fact that we are deliberately ignoring the static types that makes me say that this is a step backwards. We did a similar thing about futures in general a long time ago: The function On top of this, the type In other words, if we're using the type
"Should" doesn't matter much if reality is different. I mentioned
As I said, |
At this time, the lint no longer emits a warning for an occurrence of I still don't think |
This CL adds support for a new lint, `avoid_futureor_void`, that reports on every occurrence of the type `FutureOr<void>` in a covariant or invariant position. More details can be found at https://github.com/dart-lang/linter/issues/4622. Change-Id: I1b86e04921d1fb0b3661be091ea1f4ad72089e8b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/382403 Commit-Queue: Erik Ernst <eernst@google.com> Reviewed-by: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
avoid_futureor_void
Description
Flag the type
FutureOr<void>
as questionable.Details
The type
FutureOr<void>
is inherently a contradictory type.It is essentially the union of
Future<void>
andvoid
. However,void
is a top type so it is never possible to discriminate the union: Any object whatsoever with static typeFutureOr<void>
can be intended as thevoid
case, in which case the object should be ignored and discarded.The other interpretation (which might always be wrong) may occur if we actually have a
Future<void>
(that is, aFuture<T>
for any typeT
). In this case the object should not be ignored. We might want to await it (or pass it on to some other location where it will be handled), or we might want to.ignore()
it in order to handle the situation where it is completed with an error, but we should do something (cf., for example, https://dart.dev/tools/linter-rules/unawaited_futures and https://dart.dev/tools/linter-rules/discarded_futures).This means that any expression whose type is
FutureOr<void>
is a dilemma, and, as far as possible, we shouldn't have expressions with that type.The remedy depends on the situation: For return types, it might be possible to replace the type
FutureOr<void>
byFuture<void>?
.Future<void>?
can be discriminated precisely: If we receivednull
then we know that there is nothing to handle. Otherwise we would have received aFuture<void>
; the future should be awaited, but result that it is completed with should be discarded (or we could.ignore()
the future, but that's in itself a way to handle it). The dilemma is gone!For parameter types,
FutureOr<void>
can be replaced byvoid
: This means that the function body will not use that argument (unless it goes to great lengths to cheat us), and the type of the function will remain unchanged (according to the subtype relationships), so it won't break any clients or overrides.If the function might actually use the future (if any), it should be considered whether the parameter type can be
Future<void>?
. This is again a clear type because we can discriminate it precisely in the function body. This may be more difficult to do, because the change from parameter typeFutureOr<void>
to parameter typeFuture<void>?
makes the function a supertype of what it was previously, which is a breaking change in many ways. With the old type we could pass anything whatsoever, with the new type we can only pass null or a future. However, this might arguably be a better type for that function, because it is a less ambiguous characterization of the intended use of that parameter.Kind
Guard against logical errors where a future is ignored, but should be handled, or vice versa.
Bad Examples
Good Examples
Discussion
There may be situations where a legitimate signature using type variables yields the type
FutureOr<void>
somewhere, because a type argument turns out to have the valuevoid
. For instance, a visitor may yield a result, but some visitors exist only because of their side effects, so they could haveVisitor<void>
as a supertype, and they might have a resulting member signature whereFutureOr<void>
occurs.This is probably not easy to avoid, but it might still be helpful to mark those occurrences as special (by ignoring the lint), and comment on the right interpretation for that particular part of the interface.
Discussion checklist
return;
in function returningFutureOr<void>
. language#3246, Future<void>/FutureOr<void> require returns? #32443, Analyzer reports a return-without-value in aFutureOr<void>
function; does not report empty function #44480, Change Builder.build to FutureOr<void> build#535, Bogus void_checks warning when a FutureOr<void> callback contains a throw #58621, FutureOr<void> function warns of missing return #35237, void_checks false positive with FutureOr<void> and NNBD #58205, Analyzer: Future<void> requiring return statement? #31278, value-free return should be allowed in function which returns FutureOr<void> language#1370, Meta issue for returns in functions with void related return types #33218.The text was updated successfully, but these errors were encountered: