From 902f149e2c25926310b463a92891169f75024706 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Tue, 22 Jun 2021 20:03:13 +0000 Subject: [PATCH] Add `unawaited` function and `ignore` extensions member. The `unawaited` function in `dart:async` is intended for use with the `unawaited_futures` lint which is hopefully going to be part of the Dart recommended set of lints. The `ignore` extension method is there to provide an alternative if you even want to ignore errors from a future. By having both, it makes the distinction clearer and makes it easier to not think one can be used for everything. Change-Id: Ib96ed5ff64ead4b228721e5210efa82f76119c9f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/200428 Reviewed-by: Jacob Richman Reviewed-by: Nate Bosch Commit-Queue: Lasse R.H. Nielsen --- CHANGELOG.md | 11 ++- sdk/lib/async/future.dart | 50 +++++++++++ sdk/lib/async/future_impl.dart | 73 +++++++++++----- tests/language/static_type_helper.dart | 22 ++++- tests/language_2/static_type_helper.dart | 90 ++++++++++++++++++++ tests/lib/async/future_extension_test.dart | 54 ++++++++++++ tests/lib/async/unawaited_error_test.dart | 11 +++ tests/lib/async/unawaited_test.dart | 52 +++++++++++ tests/lib_2/async/future_extension_test.dart | 54 ++++++++++++ tests/lib_2/async/unawaited_error_test.dart | 11 +++ tests/lib_2/async/unawaited_test.dart | 54 ++++++++++++ 11 files changed, 457 insertions(+), 25 deletions(-) create mode 100644 tests/language_2/static_type_helper.dart create mode 100644 tests/lib/async/future_extension_test.dart create mode 100644 tests/lib/async/unawaited_error_test.dart create mode 100644 tests/lib/async/unawaited_test.dart create mode 100644 tests/lib_2/async/future_extension_test.dart create mode 100644 tests/lib_2/async/unawaited_error_test.dart create mode 100644 tests/lib_2/async/unawaited_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4cef9570e..7d5d4ba35da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,14 @@ #### `dart:async` -* The uncaught error handlers of `Zone`s are now run in the parent zone - of the zone where they were declared. This prevents a throwing handler - from causing an infinite loop by repeatedly triggering itself. +* The uncaught error handlers of `Zone`s are now run in the parent zone + of the zone where they were declared. This prevents a throwing handler + from causing an infinite loop by repeatedly triggering itself. + +* Added `ignore()` as extension member on futures. + +* Added `void unawaited(Future)` top-level function to deal with + the `unawaited_futures` lint. #### `dart:core` diff --git a/sdk/lib/async/future.dart b/sdk/lib/async/future.dart index 4d98a53fe41..edc62643318 100644 --- a/sdk/lib/async/future.dart +++ b/sdk/lib/async/future.dart @@ -698,6 +698,30 @@ abstract class Future { Future timeout(Duration timeLimit, {FutureOr onTimeout()?}); } +/// Explicitly ignores a future. +/// +/// Not all futures need to be awaited. +/// The Dart linter has an optional ["unawaited futures" lint](https://dart-lang.github.io/linter/lints/unawaited_futures.html) +/// which enforces that futures (expressions with a static type of [Future]) +/// in asynchronous functions are handled *somehow*. +/// If a particular future value doesn't need to be awaited, +/// you can call `unawaited(...)` with it, which will avoid the lint, +/// simply because the expression no longer has type [Future]. +/// Using `unawaited` has no other effect. +/// You should use `unawaited` to convey the *intention* of +/// deliberately not waiting for the future. +/// +/// If the future completes with an error, +/// it was likely a mistake to not await it. +/// That error will still occur and will be considered unhandled +/// unless the same future is awaited (or otherwise handled) elsewhere too. +/// Because of that, `unawaited` should only be used for futures that +/// are *expected* to complete with a value. +/// You can use [FutureExtension.ignore] if you also don't want to know +/// about errors from this future. +@Since("2.15") +void unawaited(Future future) {} + /// Convenience methods on futures. /// /// Adds functionality to futures which makes it easier to @@ -770,6 +794,32 @@ extension FutureExtensions on Future { handleError(error as E, stackTrace), test: (Object error) => error is E && (test == null || test(error))); } + + /// Completely ignores this future and its result. + /// + /// Not all futures are important, not even if they contain errors, + /// for example if a request was made, but the response is no longer needed. + /// Simply ignoring a future can result in uncaught asynchronous errors. + /// This method instead handles (and ignores) any values or errors + /// coming from this future, making it safe to otherwise ignore + /// the future. + /// + /// Use `ignore` to signal that the result of the future is + /// no longer important to the program, not even if it's an error. + /// If you merely want to silence the ["unawaited futures" lint](https://dart-lang.github.io/linter/lints/unawaited_futures.html), + /// use the [unawaited] function instead. + /// That will ensure that an unexpected error is still reported. + @Since("2.15") + void ignore() { + var self = this; + if (self is _Future) { + self._ignore(); + } else { + self.then(_ignore, onError: _ignore); + } + } + + static void _ignore(Object? _, [Object? __]) {} } /// Thrown when a scheduled timeout happens while waiting for an async result. diff --git a/sdk/lib/async/future_impl.dart b/sdk/lib/async/future_impl.dart index e73300fc6d2..c89d345182a 100644 --- a/sdk/lib/async/future_impl.dart +++ b/sdk/lib/async/future_impl.dart @@ -69,9 +69,14 @@ class _FutureListener { static const int maskTestError = 4; static const int maskWhenComplete = 8; static const int stateChain = 0; + // Handles values, passes errors on. static const int stateThen = maskValue; + // Handles values and errors. static const int stateThenOnerror = maskValue | maskError; + // Handles errors, has errorCallback. static const int stateCatchError = maskError; + // Ignores both values and errors. Has no callback or errorCallback. + // The [result] future is ignored, its always the same as the source. static const int stateCatchErrorTest = maskError | maskTestError; static const int stateWhenComplete = maskWhenComplete; static const int maskType = @@ -191,21 +196,40 @@ class _Future implements Future { /// [_FutureListener] listeners. static const int _stateIncomplete = 0; + /// Flag set when an error need not be handled. + /// + /// Set by the [FutureExtensions.ignore] method to avoid + /// having to introduce an unnecessary listener. + /// Only relevant until the future is completed. + static const int _stateIgnoreError = 1; + /// Pending completion. Set when completed using [_asyncComplete] or /// [_asyncCompleteError]. It is an error to try to complete it again. /// [_resultOrListeners] holds listeners. - static const int _statePendingComplete = 1; + static const int _statePendingComplete = 2; - /// The future has been chained to another future. The result of that - /// other future becomes the result of this future as well. + /// The future has been chained to another "source" [_Future]. + /// + /// The result of that other future becomes the result of this future + /// as well, when the other future completes. + /// This future cannot be completed again. /// [_resultOrListeners] contains the source future. - static const int _stateChained = 2; + /// Listeners have been moved to the chained future. + static const int _stateChained = 4; /// The future has been completed with a value result. - static const int _stateValue = 4; + /// + /// [_resultOrListeners] contains the value. + static const int _stateValue = 8; /// The future has been completed with an error result. - static const int _stateError = 8; + /// + /// [_resultOrListeners] contains an [AsyncEror] + /// holding the error and stack trace. + static const int _stateError = 16; + + /// Mask for the states above except [_stateIgnoreError]. + static const int _completionStateMask = 30; /// Whether the future is complete, and as what. int _state = _stateIncomplete; @@ -227,8 +251,8 @@ class _Future implements Future { /// and it is not chained to another future. /// /// The future is another future that this future is chained to. This future - /// is waiting for the other future to complete, and when it does, this future - /// will complete with the same result. + /// is waiting for the other future to complete, and when it does, + /// this future will complete with the same result. /// All listeners are forwarded to the other future. @pragma("vm:entry-point") var _resultOrListeners; @@ -253,12 +277,14 @@ class _Future implements Future { /// Creates a future that is already completed with the value. _Future.value(T value) : this.zoneValue(value, Zone._current); - bool get _mayComplete => _state == _stateIncomplete; - bool get _isPendingComplete => _state == _statePendingComplete; - bool get _mayAddListener => _state <= _statePendingComplete; - bool get _isChained => _state == _stateChained; - bool get _isComplete => _state >= _stateValue; - bool get _hasError => _state == _stateError; + bool get _mayComplete => (_state & _completionStateMask) == _stateIncomplete; + bool get _isPendingComplete => (_state & _statePendingComplete) != 0; + bool get _mayAddListener => + _state <= (_statePendingComplete | _stateIgnoreError); + bool get _isChained => (_state & _stateChained) != 0; + bool get _isComplete => (_state & (_stateValue | _stateError)) != 0; + bool get _hasError => (_state & _stateError) != 0; + bool get _ignoreError => (_state & _stateIgnoreError) != 0; static List? _continuationFunctions(_Future future) { List? result = null; @@ -283,7 +309,7 @@ class _Future implements Future { void _setChained(_Future source) { assert(_mayAddListener); - _state = _stateChained; + _state = _stateChained | (_state & _stateIgnoreError); _resultOrListeners = source; } @@ -315,6 +341,10 @@ class _Future implements Future { return result; } + void _ignore() { + _state |= _stateIgnoreError; + } + Future catchError(Function onError, {bool test(Object error)?}) { _Future result = new _Future(); if (!identical(result._zone, _rootZone)) { @@ -337,13 +367,13 @@ class _Future implements Future { Stream asStream() => new Stream.fromFuture(this); void _setPendingComplete() { - assert(_mayComplete); - _state = _statePendingComplete; + assert(_mayComplete); // Aka _statIncomplete + _state ^= _stateIncomplete ^ _statePendingComplete; } void _clearPendingComplete() { assert(_isPendingComplete); - _state = _stateIncomplete; + _state ^= _statePendingComplete ^ _stateIncomplete; } AsyncError get _error { @@ -365,7 +395,7 @@ class _Future implements Future { void _setErrorObject(AsyncError error) { assert(!_isComplete); // But may have a completion pending. - _state = _stateError; + _state = _stateError | (_state & _stateIgnoreError); _resultOrListeners = error; } @@ -379,7 +409,8 @@ class _Future implements Future { void _cloneResult(_Future source) { assert(!_isComplete); assert(source._isComplete); - _state = source._state; + _state = + (source._state & _completionStateMask) | (_state & _stateIgnoreError); _resultOrListeners = source._resultOrListeners; } @@ -615,7 +646,7 @@ class _Future implements Future { assert(source._isComplete); bool hasError = source._hasError; if (listeners == null) { - if (hasError) { + if (hasError && !source._ignoreError) { AsyncError asyncError = source._error; source._zone .handleUncaughtError(asyncError.error, asyncError.stackTrace); diff --git a/tests/language/static_type_helper.dart b/tests/language/static_type_helper.dart index 94843878ee6..4a7809ac76c 100644 --- a/tests/language/static_type_helper.dart +++ b/tests/language/static_type_helper.dart @@ -2,7 +2,10 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -// Ensures a context type of [T] for the operand. +/// Helper to create [Type] values. +Type typeOf() => T; + +/// Ensures a context type of [T] for the operand. void context(T x) {} /// Captures the context type of the call and returns the same type. @@ -34,6 +37,23 @@ extension StaticType on T { T expectStaticType>() { return this; } + + /// Invokes [callback] with the static type of `this`. + /// + /// Allows any operation on the type. + T captureStaticType(void Function() callback) { + callback(); + return this; + } +} + +/// Invokes [callback] with the static type of [value]. +/// +/// Similar to [StaticType.captureStaticType], but works +/// for types like `void` and `dynamic` which do not allow +/// extension methods. +void captureStaticType(T value, void Function(X value) callback) { + callback(value); } /// Use with [StaticType.expectStaticType] to expect precisely the type [T]. diff --git a/tests/language_2/static_type_helper.dart b/tests/language_2/static_type_helper.dart new file mode 100644 index 00000000000..4a7809ac76c --- /dev/null +++ b/tests/language_2/static_type_helper.dart @@ -0,0 +1,90 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Helper to create [Type] values. +Type typeOf() => T; + +/// Ensures a context type of [T] for the operand. +void context(T x) {} + +/// Captures the context type of the call and returns the same type. +/// +/// Can be used to check the context type as: +/// ```dart +/// int x = contextType(1 /* valid value */)..expectStaticType>; +/// ``` +T contextType(Object result) => result as T; + +extension StaticType on T { + /// Check the static type. + /// + /// Use as follows (assuming `e` has static type `num`): + /// ```dart + /// e.expectStaticType>() // No context type. + /// e.expectStaticType>() // No context type. + /// e.expectStaticType>() // No context type. + /// ``` + /// or + /// ```dart + /// e..expectStaticType>() // Preserve context type. + /// e..expectStaticType>() // Preserve context type. + /// e..expectStaticType>() // Preserve context type. + /// ``` + /// This will be a *compile-time error* if the static type is not + /// as required by the constraints type (the one passed to [Exactly], + /// [SubtypeOf] or [SupertypeOf].) + T expectStaticType>() { + return this; + } + + /// Invokes [callback] with the static type of `this`. + /// + /// Allows any operation on the type. + T captureStaticType(void Function() callback) { + callback(); + return this; + } +} + +/// Invokes [callback] with the static type of [value]. +/// +/// Similar to [StaticType.captureStaticType], but works +/// for types like `void` and `dynamic` which do not allow +/// extension methods. +void captureStaticType(T value, void Function(X value) callback) { + callback(value); +} + +/// Use with [StaticType.expectStaticType] to expect precisely the type [T]. +/// +/// Example use: +/// ```dart +/// "abc".expectStaticType>(); +/// ``` +typedef Exactly = T Function(T); + +/// Use with [StaticType.expectStaticType] to expect a subtype of [T]. +/// +/// Example use: +/// ```dart +/// num x = 1; +/// x.expectStaticType>(); +/// ``` +typedef SubtypeOf = Never Function(T); + +/// Use with [StaticType.expectStaticType] to expect a supertype of [T]. +/// +/// Example use: +/// ```dart +/// num x = 1; +/// x.expectStaticType>(); +/// ``` +typedef SupertypeOf = T Function(Object?); + +/// Checks that an expression is assignable to [T1], [T2] and [Object]. +/// +/// This ensures that the static type of the expression is either dynamic, +/// Never, or a type assignable to both [T1] and [T2], and if those are +/// unrelated, it must be an intersection type. +void checkIntersectionType(T1 v1, T2 v2, Object v3) {} diff --git a/tests/lib/async/future_extension_test.dart b/tests/lib/async/future_extension_test.dart new file mode 100644 index 00000000000..cd28986fd9e --- /dev/null +++ b/tests/lib/async/future_extension_test.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async_helper/async_helper.dart'; +import "package:expect/expect.dart"; +import 'dart:async' show Completer, runZonedGuarded; +import '../../language/static_type_helper.dart'; + +void main() { + testIgnore(); +} + +void testIgnore() { + var future = Future.value(42); + captureStaticType(future.ignore(), (T value) { + Expect.equals(typeOf(), T); + }); + + asyncStart(); + // Ignored futures can still be listend to. + { + var c = Completer.sync(); + var f = c.future; + f.ignore(); + asyncStart(); + f.catchError((e) { + Expect.equals("ERROR1", e); + asyncEnd(); + return 0; + }); + c.completeError("ERROR1"); + } + + // Ignored futures are not uncaught errors. + { + asyncStart(); + bool threw = false; + runZonedGuarded(() { + var c = Completer.sync(); + var f = c.future; + f.ignore(); + c.completeError("ERROR2"); + }, (e, s) { + threw = true; + Expect.fail("Should not happen: $e"); + }); + Future.delayed(Duration.zero, () { + if (threw) Expect.fail("Future not ignored."); + asyncEnd(); + }); + } + asyncEnd(); +} diff --git a/tests/lib/async/unawaited_error_test.dart b/tests/lib/async/unawaited_error_test.dart new file mode 100644 index 00000000000..597a9f84d1c --- /dev/null +++ b/tests/lib/async/unawaited_error_test.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +void main() { + // The `unawaited` function is not exposed by dart:core. + unawaited; + // [error line 7, column 3, length 9] + // [cfe] Getter not found: 'unawaited'. + // [analyzer] COMPILE_TIME_ERROR.UNDEFINED_IDENTIFIER +} diff --git a/tests/lib/async/unawaited_test.dart b/tests/lib/async/unawaited_test.dart new file mode 100644 index 00000000000..dc095cc5a3c --- /dev/null +++ b/tests/lib/async/unawaited_test.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async_helper/async_helper.dart'; +import "package:expect/expect.dart"; +import 'dart:async' show Completer, runZonedGuarded, unawaited; +import 'dart:async' as prefix; +import '../../language/static_type_helper.dart'; + +void main() { + testUnawaited(); +} + +void testUnawaited() { + // Exists where expected. + prefix.unawaited.expectStaticType)>>(); + + var future = Future.value(42); + captureStaticType(unawaited(future), (value) { + Expect.equals(typeOf(), T); + }); + + asyncStart(); + // Unawaited futures still throw. + { + var c = Completer(); + var f = c.future; + unawaited(f); + asyncStart(); + f.catchError((e) { + Expect.equals("ERROR1", e); + asyncEnd(); + return 0; + }); + c.completeError("ERROR1"); + } + // Unawaited futures are still uncaught errors. + { + asyncStart(); + runZonedGuarded(() { + var c = Completer(); + var f = c.future; + unawaited(f); + c.completeError("ERROR2"); + }, (e, s) { + Expect.equals("ERROR2", e); + asyncEnd(); + }); + } + asyncEnd(); +} diff --git a/tests/lib_2/async/future_extension_test.dart b/tests/lib_2/async/future_extension_test.dart new file mode 100644 index 00000000000..cd28986fd9e --- /dev/null +++ b/tests/lib_2/async/future_extension_test.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async_helper/async_helper.dart'; +import "package:expect/expect.dart"; +import 'dart:async' show Completer, runZonedGuarded; +import '../../language/static_type_helper.dart'; + +void main() { + testIgnore(); +} + +void testIgnore() { + var future = Future.value(42); + captureStaticType(future.ignore(), (T value) { + Expect.equals(typeOf(), T); + }); + + asyncStart(); + // Ignored futures can still be listend to. + { + var c = Completer.sync(); + var f = c.future; + f.ignore(); + asyncStart(); + f.catchError((e) { + Expect.equals("ERROR1", e); + asyncEnd(); + return 0; + }); + c.completeError("ERROR1"); + } + + // Ignored futures are not uncaught errors. + { + asyncStart(); + bool threw = false; + runZonedGuarded(() { + var c = Completer.sync(); + var f = c.future; + f.ignore(); + c.completeError("ERROR2"); + }, (e, s) { + threw = true; + Expect.fail("Should not happen: $e"); + }); + Future.delayed(Duration.zero, () { + if (threw) Expect.fail("Future not ignored."); + asyncEnd(); + }); + } + asyncEnd(); +} diff --git a/tests/lib_2/async/unawaited_error_test.dart b/tests/lib_2/async/unawaited_error_test.dart new file mode 100644 index 00000000000..597a9f84d1c --- /dev/null +++ b/tests/lib_2/async/unawaited_error_test.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +void main() { + // The `unawaited` function is not exposed by dart:core. + unawaited; + // [error line 7, column 3, length 9] + // [cfe] Getter not found: 'unawaited'. + // [analyzer] COMPILE_TIME_ERROR.UNDEFINED_IDENTIFIER +} diff --git a/tests/lib_2/async/unawaited_test.dart b/tests/lib_2/async/unawaited_test.dart new file mode 100644 index 00000000000..16bd285103a --- /dev/null +++ b/tests/lib_2/async/unawaited_test.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// @dart = 2.9 + +import 'package:async_helper/async_helper.dart'; +import "package:expect/expect.dart"; +import 'dart:async' show Completer, runZonedGuarded, unawaited; +import 'dart:async' as prefix; +import '../../language/static_type_helper.dart'; + +void main() { + testUnawaited(); +} + +void testUnawaited() { + // Exists where expected. + prefix.unawaited.expectStaticType)>>(); + + var future = Future.value(42); + captureStaticType(unawaited(future), (value) { + Expect.equals(typeOf(), T); + }); + + asyncStart(); + // Unawaited futures still throw. + { + var c = Completer(); + var f = c.future; + unawaited(f); + asyncStart(); + f.catchError((e) { + Expect.equals("ERROR1", e); + asyncEnd(); + return 0; + }); + c.completeError("ERROR1"); + } + // Unawaited futures are still uncaught errors. + { + asyncStart(); + runZonedGuarded(() { + var c = Completer(); + var f = c.future; + unawaited(f); + c.completeError("ERROR2"); + }, (e, s) { + Expect.equals("ERROR2", e); + asyncEnd(); + }); + } + asyncEnd(); +}