Skip to content

Surprising wrapping of Future<X?> within Future<Object> #48845

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

Open
kpsroka opened this issue Apr 20, 2022 · 2 comments
Open

Surprising wrapping of Future<X?> within Future<Object> #48845

kpsroka opened this issue Apr 20, 2022 · 2 comments
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-warning Issues with the analyzer's Warning codes P2 A bug or feature request we're likely to work on type-enhancement A request for a change that isn't a bug

Comments

@kpsroka
Copy link

kpsroka commented Apr 20, 2022

I've encountered a weird bug stemming from Dart's handling of generics/futures. Consider the following snippet:

Future<void> main() async {
  Future<Object?> _nestedFuture() => Future.value('test');

  Future<Object> _promoted() async {
    return _nestedFuture();
  }
  
  Future<Object?> _notPromoted() async {
    return _nestedFuture();
  }
  
  print('await _promoted() returns ${await _promoted()}');
  print('await _notPromoted() returns ${await _notPromoted()}');
}

Here the _promoted() method returns a Future wrapping the future from _nestedFuture(), while the _notPromoted() method returns the result of _nestedFuture() directly. The issue lies in the fact that Future<Object?> is not assignable to Future<Object>, but is assignable to Object, and hence the compiler wraps one future in another, and await _promoted() returns the future from _nestedFuture().

It might not be against specification of the Dart language, but I would appreciate if such wrapping returned an analyzer warning about a potential misuse of Futures.

Dart SDK version: 2.16.2 (stable), reproducible on DartPad as well as Android/JIT (haven't checked other platforms).

@lrhn
Copy link
Member

lrhn commented Apr 20, 2022

This is a tricky problem because of the way async return works.

The type check which makes the object get wrapped happens at runtime, during the code which implicitly awaits the future being returned. The return _nestedFuture() in _promoted is going to do:

  FutureOr<Object> $actualValue = _nestedFuture(); // of type Future<Object>, which is assignable to Object.
  if ($actualValue is Future<Object>) return await $actualValue; // which completes the returned future with that value.
  return $actualValue;

Here the value will always be a Future<Object?>, but the static type system alone can't know that the actual value won't be an instance of the subtype Future<Object>, so it has to check at runtime.

The thing we could decide to warn about, and probably should (unless we just fix the linked issue entirely so you have to write the await yourself, and can't return a Future<T> where a T is needed!), is when you return something which is potentially a Future, and typed as such, which potentially won't be awaited by the return.

Or, in other words, if the return type of the async function is Future<T> and the expression of a return statement has type Future<X> or FutureOr<X> and X is not a subtype of T, warn that a future might get returned as a value.
(If the type of the return expression is just Object, it can still be a future, but we don't have any hint that it will.)

@lrhn lrhn added legacy-area-analyzer Use area-devexp instead. type-enhancement A request for a change that isn't a bug labels Apr 20, 2022
@srawlins srawlins added devexp-warning Issues with the analyzer's Warning codes P2 A bug or feature request we're likely to work on labels Apr 20, 2022
@eernstg
Copy link
Member

eernstg commented Apr 21, 2022

I think it's worth noting that this situation only arises when the 'future value type' of the asynchronous function is Object, which is essentially the same thing as when the declared return type is Future<Object>, and also only when the returned object is a Future<Top> where Top is a top type (a supertype of all other types, like Object? or void). So it's extremely narrow.

Future<Object> f() async {
  return e; // Assume that `e` evaluates to an object `o` of type `Future<T>` for some `T`.
}

If the future value type had been a proper supertype of Object then the declared return type would have been Future<Top> for some top type Top, or it could be a supertype thereof. In this case the returned object will be obtained by await o, because Future<T> is always a subtype of Future<Top>.

If the future value type had been a proper subtype of Object then it could still have been a supertype of Future<T> (say, the declared return type could have been Future<Future<Object?>>), in which case it would be OK to use o to complete the returned future. Otherwise (when the future value type isn't a supertype of Future<T>), we could have the situation where T is a subtype of the future value type, so we'll await; otherwise, a dynamic error would occur (or a compile-time error, depending on the use of dynamic etc). That's also OK.

So we only have a surprising behavior in the very specific case where the future value type is Object and the returned object has type Future<Top> where Top is a top type. So we could lint the following:

  • An asynchronous non-generator function has future value type Object.
  • A return e; occurs in its body, and the static type of e is Future<Top> for some top type Top, or a supertype thereof which is not dynamic.

In this case the returned future could be completed with a Future<Object?>, but the intention might be that this future should be awaited to yield an object o, and the returned future should be completed with o.

@bwilkerson bwilkerson added area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. and removed legacy-area-analyzer Use area-devexp instead. labels Feb 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-warning Issues with the analyzer's Warning codes P2 A bug or feature request we're likely to work on type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

5 participants