analyzer/linter: Support an @awaitNotRequired annotation
Work towards https://github.com/dart-lang/sdk/issues/46218
This implements the bulk of the `@awaitNotRequired` support as specified here: https://github.com/dart-lang/sdk/issues/46218#issuecomment-2725895048
Remaining work:
* implement inheritence
* report a badly-placed annotation
* add the annotation to the real meta package
Change-Id: Ida16018746704d0baa165ab9be95187b52a79061
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423923
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analyzer/api.txt b/pkg/analyzer/api.txt
index 0d7b7db..50236f4 100644
--- a/pkg/analyzer/api.txt
+++ b/pkg/analyzer/api.txt
@@ -2983,6 +2983,7 @@
constantEvaluationErrors (getter: List<AnalysisError>?)
element2 (getter: Element2?)
isAlwaysThrows (getter: bool)
+ isAwaitNotRequired (getter: bool)
isDeprecated (getter: bool)
isDoNotStore (getter: bool)
isDoNotSubmit (getter: bool)
@@ -3563,6 +3564,7 @@
new (constructor: Metadata Function())
annotations (getter: List<ElementAnnotation>)
hasAlwaysThrows (getter: bool)
+ hasAwaitNotRequired (getter: bool)
hasDeprecated (getter: bool)
hasDoNotStore (getter: bool)
hasDoNotSubmit (getter: bool)
diff --git a/pkg/analyzer/lib/dart/element/element.dart b/pkg/analyzer/lib/dart/element/element.dart
index 930443c..fa64210 100644
--- a/pkg/analyzer/lib/dart/element/element.dart
+++ b/pkg/analyzer/lib/dart/element/element.dart
@@ -115,6 +115,10 @@
/// Whether the annotation marks the associated function as always throwing.
bool get isAlwaysThrows;
+ /// Whether the annotation marks the associated element as not needing to be
+ /// awaited.
+ bool get isAwaitNotRequired;
+
/// Whether the annotation marks the associated element as being deprecated.
bool get isDeprecated;
diff --git a/pkg/analyzer/lib/dart/element/element2.dart b/pkg/analyzer/lib/dart/element/element2.dart
index a2e53f1..4343e0d 100644
--- a/pkg/analyzer/lib/dart/element/element2.dart
+++ b/pkg/analyzer/lib/dart/element/element2.dart
@@ -2139,6 +2139,9 @@
/// Whether the receiver has an annotation of the form `@alwaysThrows`.
bool get hasAlwaysThrows;
+ /// Whether the receiver has an annotation of the form `@awaitNotRequired`.
+ bool get hasAwaitNotRequired;
+
/// Whether the receiver has an annotation of the form `@deprecated`
/// or `@Deprecated('..')`.
bool get hasDeprecated;
diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart
index 55bd0b1..bf35e97 100644
--- a/pkg/analyzer/lib/src/dart/element/element.dart
+++ b/pkg/analyzer/lib/src/dart/element/element.dart
@@ -1992,6 +1992,10 @@
/// throws, for dead code purposes.
static const String _alwaysThrowsVariableName = 'alwaysThrows';
+ /// The name of the top-level variable used to mark an element as not needing
+ /// to be awaited.
+ static const String _awaitNotRequiredVariableName = 'awaitNotRequired';
+
/// The name of the class used to mark an element as being deprecated.
static const String _deprecatedClassName = 'Deprecated';
@@ -2185,6 +2189,10 @@
bool get isAlwaysThrows => _isPackageMetaGetter(_alwaysThrowsVariableName);
@override
+ bool get isAwaitNotRequired =>
+ _isPackageMetaGetter(_awaitNotRequiredVariableName);
+
+ @override
bool get isConstantEvaluated => evaluationResult != null;
bool get isDartInternalSince {
@@ -7138,6 +7146,18 @@
}
@override
+ bool get hasAwaitNotRequired {
+ var annotations = this.annotations;
+ for (var i = 0; i < annotations.length; i++) {
+ var annotation = annotations[i];
+ if (annotation.isAwaitNotRequired) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @override
bool get hasDeprecated {
if (_metadataFlags < 0) {
// TODO(augmentations): Consider optimizing this similar to `ElementImpl`.
diff --git a/pkg/analyzer/lib/src/error/annotation_verifier.dart b/pkg/analyzer/lib/src/error/annotation_verifier.dart
index dde9b49..c1ff62e 100644
--- a/pkg/analyzer/lib/src/error/annotation_verifier.dart
+++ b/pkg/analyzer/lib/src/error/annotation_verifier.dart
@@ -43,7 +43,10 @@
return;
}
var parent = node.parent;
- if (element.isFactory) {
+ if (element.isAwaitNotRequired) {
+ // TODO(srawlins): Check that functions return Future and fields and
+ // getters have a static type of Future.
+ } else if (element.isFactory) {
_checkFactory(node);
} else if (element.isInternal) {
_checkInternal(node);
diff --git a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/meta/lib/meta.dart b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/meta/lib/meta.dart
index ebc7ed6..46c7639 100644
--- a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/meta/lib/meta.dart
+++ b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/meta/lib/meta.dart
@@ -58,6 +58,38 @@
@Deprecated("Use a return type of 'Never' instead")
const _AlwaysThrows alwaysThrows = _AlwaysThrows();
+/// Used to annotate a [Future]-returning function (including constructors,
+/// getters, methods, and operators), or a [Future]-typed field (including
+/// top-level, instance, and static) `f`. Indicates that the [Future] value that
+/// `f` returns does not need to be awaited. Any methods that override `f` in
+/// class inheritance, are also expected to conform to this contract.
+///
+/// Tools, such as the analyzer, can use this to understand whether to report
+/// that a [Future]-typed value does not need to be awaited:
+///
+/// ```dart
+/// @awaitNotRequired Future<LogMessage> log(String message) { ... }
+///
+/// int fn() {
+/// log('Message'); // Not important to wait for logging to complete.
+/// }
+/// ```
+///
+/// Without the annotation on `log`, the analyzer may report a lint diagnostic
+/// at the call to `log`, such as `discarded_futures` or `unawaited_futures`,
+/// regarding the danger of not awaiting the function call, depending on what
+/// lint rules are enabled.
+///
+/// Tools, such as the analyzer, can also provide feedback if
+///
+/// * the annotation is associated with anything other than a constructor,
+/// function, method, operator, field, or top-level variable, or
+/// * the annotation is associated with a constructor, function, method, or
+/// operator that does not return a [Future], or
+/// * the annotation is associated with a field or top-level variable that is
+/// not typed as a [Future].
+const _AwaitNotRequired awaitNotRequired = _AwaitNotRequired();
+
/// Used to annotate a parameter of an instance method that overrides another
/// method.
///
@@ -621,6 +653,19 @@
const _AlwaysThrows();
}
+/// See [awaitNotRequired] for more details.
+@Target({
+ TargetKind.constructor,
+ TargetKind.field,
+ TargetKind.function,
+ TargetKind.getter,
+ TargetKind.method,
+ TargetKind.topLevelVariable,
+})
+class _AwaitNotRequired {
+ const _AwaitNotRequired();
+}
+
class _Checked {
const _Checked();
}
diff --git a/pkg/linter/lib/src/rules/discarded_futures.dart b/pkg/linter/lib/src/rules/discarded_futures.dart
index 1046428..cafa370 100644
--- a/pkg/linter/lib/src/rules/discarded_futures.dart
+++ b/pkg/linter/lib/src/rules/discarded_futures.dart
@@ -70,6 +70,8 @@
_reportOnExpression(expr);
}
+
+ // TODO(srawlins): Take `@awaitNotRequired` into account.
}
@override
@@ -114,6 +116,7 @@
if ((expr.staticType.isFutureOrFutureOr) &&
!_isEnclosedInAsyncFunctionBody(expr) &&
expr is! AssignmentExpression) {
+ // TODO(srawlins): Take `@awaitNotRequired` into account.
_reportOnExpression(expr);
}
}
diff --git a/pkg/linter/lib/src/rules/unawaited_futures.dart b/pkg/linter/lib/src/rules/unawaited_futures.dart
index f00646e..2061707 100644
--- a/pkg/linter/lib/src/rules/unawaited_futures.dart
+++ b/pkg/linter/lib/src/rules/unawaited_futures.dart
@@ -64,6 +64,10 @@
return;
}
+ if (_isAwaitNotRequired(expr)) {
+ return;
+ }
+
if (_isEnclosedInAsyncFunctionBody(node)) {
// Future expression statement that isn't awaited in an async function:
// while this is legal, it's a very frequent sign of an error.
@@ -76,6 +80,39 @@
_visit(node.expression);
}
+ bool _isAwaitNotRequired(Expression node) {
+ // TODO(srawlins): Handle inheritence in each of these cases; the
+ // `@awaitNotRequired` annotation should be inherited.
+ switch (node) {
+ case BinaryExpression():
+ if (node.element.hasAwaitNotRequired) {
+ return true;
+ }
+
+ case MethodInvocation():
+ if (node.methodName.element.hasAwaitNotRequired) {
+ return true;
+ }
+
+ case PrefixedIdentifier():
+ if (node.identifier.element.hasAwaitNotRequired) {
+ return true;
+ }
+
+ case PrefixExpression():
+ if (node.element.hasAwaitNotRequired) {
+ return true;
+ }
+
+ case PropertyAccess():
+ if (node.propertyName.element.hasAwaitNotRequired) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
bool _isEnclosedInAsyncFunctionBody(AstNode node) {
var enclosingFunctionBody = node.thisOrAncestorOfType<FunctionBody>();
return enclosingFunctionBody?.isAsynchronous ?? false;
@@ -99,6 +136,10 @@
_isMapClass(expr.methodName.element?.enclosingElement2);
void _visit(Expression expr) {
+ if (_isAwaitNotRequired(expr)) {
+ return;
+ }
+
// TODO(srawlins): Check whether `expr`'s static type _implements_ `Future`.
if ((expr.staticType?.isDartAsyncFuture ?? false) &&
_isEnclosedInAsyncFunctionBody(expr) &&
@@ -107,3 +148,9 @@
}
}
}
+
+extension on Element2? {
+ bool get hasAwaitNotRequired =>
+ this is Annotatable &&
+ (this! as Annotatable).metadata2.hasAwaitNotRequired;
+}
diff --git a/pkg/linter/test/rules/unawaited_futures_test.dart b/pkg/linter/test/rules/unawaited_futures_test.dart
index 32d7554..2aa0854 100644
--- a/pkg/linter/test/rules/unawaited_futures_test.dart
+++ b/pkg/linter/test/rules/unawaited_futures_test.dart
@@ -15,6 +15,9 @@
@reflectiveTest
class UnawaitedFuturesTest extends LintRuleTest {
@override
+ bool get addMetaPackageDep => true;
+
+ @override
String get lintRule => LintNames.unawaited_futures;
test_binaryExpression_unawaited() async {
@@ -31,6 +34,19 @@
);
}
+ test_binaryExpression_unawaited_awaitNotRequired() async {
+ await assertNoDiagnostics(r'''
+import 'package:meta/meta.dart';
+void f(C a, C b) async {
+ a + b;
+}
+class C {
+ @awaitNotRequired
+ Future<int> operator +(C other) async => 7;
+}
+''');
+ }
+
test_boundToFuture_unawaited() async {
// This behavior was not necessarily designed, but this test documents the
// current behavior.
@@ -96,6 +112,17 @@
);
}
+ test_functionCall_interpolated_unawaited_awaitNotRequired() async {
+ await assertNoDiagnostics(r'''
+import 'package:meta/meta.dart';
+void f() async {
+ '${g()}';
+}
+@awaitNotRequired
+Future<int> g() => Future.value(0);
+''');
+ }
+
test_functionCall_nullableFuture_unawaited() async {
await assertDiagnostics(
r'''
@@ -129,6 +156,33 @@
);
}
+ test_functionCall_unawaited_awaitNotRequired() async {
+ await assertNoDiagnostics(r'''
+import 'package:meta/meta.dart';
+void f() async {
+ g();
+}
+@awaitNotRequired
+Future<int> g() => Future.value(0);
+''');
+ }
+
+ test_functionCallInCascade() async {
+ await assertDiagnostics(
+ r'''
+void f() async {
+ C()..doAsync();
+}
+class C {
+ Future<void> doAsync() async {}
+}
+''',
+ [lint(22, 11)],
+ );
+ }
+
+ // TODO(srawlins): Test that `@awaitNotRequired` is inherited.
+
test_functionCallInCascade_assignment() async {
await assertNoDiagnostics(r'''
void f() async {
@@ -141,18 +195,17 @@
''');
}
- test_functionCallInCascade_inAsync() async {
- await assertDiagnostics(
- r'''
+ test_functionCallInCascade_awaitNotRequired() async {
+ await assertNoDiagnostics(r'''
+import 'package:meta/meta.dart';
void f() async {
C()..doAsync();
}
class C {
+ @awaitNotRequired
Future<void> doAsync() async {}
}
-''',
- [lint(22, 11)],
- );
+''');
}
test_functionCallInCascade_indexAssignment() async {
@@ -180,6 +233,19 @@
}
test_instanceProperty_unawaited() async {
+ await assertNoDiagnostics(r'''
+import 'package:meta/meta.dart';
+void f(C c) async {
+ c.p;
+}
+abstract class C {
+ @awaitNotRequired
+ Future<int> get p;
+}
+''');
+ }
+
+ test_instanceProperty_unawaited_awaitNotRequired() async {
await assertDiagnostics(
r'''
void f(C c) async {
@@ -204,7 +270,7 @@
);
}
- test_unaryExpression_unawaited() async {
+ test_prefixExpression_unawaited() async {
await assertDiagnostics(
r'''
void f(C a) async {
@@ -218,6 +284,19 @@
);
}
+ test_prefixExpression_unawaited_awaitNotRequired() async {
+ await assertNoDiagnostics(r'''
+import 'package:meta/meta.dart';
+void f(C a) async {
+ -a;
+}
+class C {
+ @awaitNotRequired
+ Future<int> operator -() async => 7;
+}
+''');
+ }
+
test_undefinedIdentifier() async {
await assertDiagnostics(
r'''