Skip to content

Commit 4146134

Browse files
srawlinsCommit Queue
authored andcommitted
analyzer/linter: Support an @awaitNotRequired annotation
Work towards #46218 This implements the bulk of the `@awaitNotRequired` support as specified here: #46218 (comment) 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 <[email protected]> Commit-Queue: Samuel Rawlins <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent e88162b commit 4146134

File tree

9 files changed

+214
-8
lines changed

9 files changed

+214
-8
lines changed

pkg/analyzer/api.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2983,6 +2983,7 @@ package:analyzer/dart/element/element.dart:
29832983
constantEvaluationErrors (getter: List<AnalysisError>?)
29842984
element2 (getter: Element2?)
29852985
isAlwaysThrows (getter: bool)
2986+
isAwaitNotRequired (getter: bool)
29862987
isDeprecated (getter: bool)
29872988
isDoNotStore (getter: bool)
29882989
isDoNotSubmit (getter: bool)
@@ -3563,6 +3564,7 @@ package:analyzer/dart/element/element2.dart:
35633564
new (constructor: Metadata Function())
35643565
annotations (getter: List<ElementAnnotation>)
35653566
hasAlwaysThrows (getter: bool)
3567+
hasAwaitNotRequired (getter: bool)
35663568
hasDeprecated (getter: bool)
35673569
hasDoNotStore (getter: bool)
35683570
hasDoNotSubmit (getter: bool)

pkg/analyzer/lib/dart/element/element.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ abstract class ElementAnnotation implements ConstantEvaluationTarget {
115115
/// Whether the annotation marks the associated function as always throwing.
116116
bool get isAlwaysThrows;
117117

118+
/// Whether the annotation marks the associated element as not needing to be
119+
/// awaited.
120+
bool get isAwaitNotRequired;
121+
118122
/// Whether the annotation marks the associated element as being deprecated.
119123
bool get isDeprecated;
120124

pkg/analyzer/lib/dart/element/element2.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,6 +2139,9 @@ abstract class Metadata {
21392139
/// Whether the receiver has an annotation of the form `@alwaysThrows`.
21402140
bool get hasAlwaysThrows;
21412141

2142+
/// Whether the receiver has an annotation of the form `@awaitNotRequired`.
2143+
bool get hasAwaitNotRequired;
2144+
21422145
/// Whether the receiver has an annotation of the form `@deprecated`
21432146
/// or `@Deprecated('..')`.
21442147
bool get hasDeprecated;

pkg/analyzer/lib/src/dart/element/element.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1992,6 +1992,10 @@ class ElementAnnotationImpl implements ElementAnnotation {
19921992
/// throws, for dead code purposes.
19931993
static const String _alwaysThrowsVariableName = 'alwaysThrows';
19941994

1995+
/// The name of the top-level variable used to mark an element as not needing
1996+
/// to be awaited.
1997+
static const String _awaitNotRequiredVariableName = 'awaitNotRequired';
1998+
19951999
/// The name of the class used to mark an element as being deprecated.
19962000
static const String _deprecatedClassName = 'Deprecated';
19972001

@@ -2184,6 +2188,10 @@ class ElementAnnotationImpl implements ElementAnnotation {
21842188
@override
21852189
bool get isAlwaysThrows => _isPackageMetaGetter(_alwaysThrowsVariableName);
21862190

2191+
@override
2192+
bool get isAwaitNotRequired =>
2193+
_isPackageMetaGetter(_awaitNotRequiredVariableName);
2194+
21872195
@override
21882196
bool get isConstantEvaluated => evaluationResult != null;
21892197

@@ -7137,6 +7145,18 @@ final class MetadataImpl implements Metadata {
71377145
return false;
71387146
}
71397147

7148+
@override
7149+
bool get hasAwaitNotRequired {
7150+
var annotations = this.annotations;
7151+
for (var i = 0; i < annotations.length; i++) {
7152+
var annotation = annotations[i];
7153+
if (annotation.isAwaitNotRequired) {
7154+
return true;
7155+
}
7156+
}
7157+
return false;
7158+
}
7159+
71407160
@override
71417161
bool get hasDeprecated {
71427162
if (_metadataFlags < 0) {

pkg/analyzer/lib/src/error/annotation_verifier.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ class AnnotationVerifier {
4343
return;
4444
}
4545
var parent = node.parent;
46-
if (element.isFactory) {
46+
if (element.isAwaitNotRequired) {
47+
// TODO(srawlins): Check that functions return Future and fields and
48+
// getters have a static type of Future.
49+
} else if (element.isFactory) {
4750
_checkFactory(node);
4851
} else if (element.isInternal) {
4952
_checkInternal(node);

pkg/analyzer_utilities/lib/test/mock_packages/package_content/meta/lib/meta.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,38 @@ import 'meta_meta.dart';
5858
@Deprecated("Use a return type of 'Never' instead")
5959
const _AlwaysThrows alwaysThrows = _AlwaysThrows();
6060

61+
/// Used to annotate a [Future]-returning function (including constructors,
62+
/// getters, methods, and operators), or a [Future]-typed field (including
63+
/// top-level, instance, and static) `f`. Indicates that the [Future] value that
64+
/// `f` returns does not need to be awaited. Any methods that override `f` in
65+
/// class inheritance, are also expected to conform to this contract.
66+
///
67+
/// Tools, such as the analyzer, can use this to understand whether to report
68+
/// that a [Future]-typed value does not need to be awaited:
69+
///
70+
/// ```dart
71+
/// @awaitNotRequired Future<LogMessage> log(String message) { ... }
72+
///
73+
/// int fn() {
74+
/// log('Message'); // Not important to wait for logging to complete.
75+
/// }
76+
/// ```
77+
///
78+
/// Without the annotation on `log`, the analyzer may report a lint diagnostic
79+
/// at the call to `log`, such as `discarded_futures` or `unawaited_futures`,
80+
/// regarding the danger of not awaiting the function call, depending on what
81+
/// lint rules are enabled.
82+
///
83+
/// Tools, such as the analyzer, can also provide feedback if
84+
///
85+
/// * the annotation is associated with anything other than a constructor,
86+
/// function, method, operator, field, or top-level variable, or
87+
/// * the annotation is associated with a constructor, function, method, or
88+
/// operator that does not return a [Future], or
89+
/// * the annotation is associated with a field or top-level variable that is
90+
/// not typed as a [Future].
91+
const _AwaitNotRequired awaitNotRequired = _AwaitNotRequired();
92+
6193
/// Used to annotate a parameter of an instance method that overrides another
6294
/// method.
6395
///
@@ -621,6 +653,19 @@ class _AlwaysThrows {
621653
const _AlwaysThrows();
622654
}
623655

656+
/// See [awaitNotRequired] for more details.
657+
@Target({
658+
TargetKind.constructor,
659+
TargetKind.field,
660+
TargetKind.function,
661+
TargetKind.getter,
662+
TargetKind.method,
663+
TargetKind.topLevelVariable,
664+
})
665+
class _AwaitNotRequired {
666+
const _AwaitNotRequired();
667+
}
668+
624669
class _Checked {
625670
const _Checked();
626671
}

pkg/linter/lib/src/rules/discarded_futures.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class _Visitor extends SimpleAstVisitor<void> {
7070

7171
_reportOnExpression(expr);
7272
}
73+
74+
// TODO(srawlins): Take `@awaitNotRequired` into account.
7375
}
7476

7577
@override
@@ -114,6 +116,7 @@ class _Visitor extends SimpleAstVisitor<void> {
114116
if ((expr.staticType.isFutureOrFutureOr) &&
115117
!_isEnclosedInAsyncFunctionBody(expr) &&
116118
expr is! AssignmentExpression) {
119+
// TODO(srawlins): Take `@awaitNotRequired` into account.
117120
_reportOnExpression(expr);
118121
}
119122
}

pkg/linter/lib/src/rules/unawaited_futures.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ class _Visitor extends SimpleAstVisitor<void> {
6464
return;
6565
}
6666

67+
if (_isAwaitNotRequired(expr)) {
68+
return;
69+
}
70+
6771
if (_isEnclosedInAsyncFunctionBody(node)) {
6872
// Future expression statement that isn't awaited in an async function:
6973
// while this is legal, it's a very frequent sign of an error.
@@ -76,6 +80,39 @@ class _Visitor extends SimpleAstVisitor<void> {
7680
_visit(node.expression);
7781
}
7882

83+
bool _isAwaitNotRequired(Expression node) {
84+
// TODO(srawlins): Handle inheritence in each of these cases; the
85+
// `@awaitNotRequired` annotation should be inherited.
86+
switch (node) {
87+
case BinaryExpression():
88+
if (node.element.hasAwaitNotRequired) {
89+
return true;
90+
}
91+
92+
case MethodInvocation():
93+
if (node.methodName.element.hasAwaitNotRequired) {
94+
return true;
95+
}
96+
97+
case PrefixedIdentifier():
98+
if (node.identifier.element.hasAwaitNotRequired) {
99+
return true;
100+
}
101+
102+
case PrefixExpression():
103+
if (node.element.hasAwaitNotRequired) {
104+
return true;
105+
}
106+
107+
case PropertyAccess():
108+
if (node.propertyName.element.hasAwaitNotRequired) {
109+
return true;
110+
}
111+
}
112+
113+
return false;
114+
}
115+
79116
bool _isEnclosedInAsyncFunctionBody(AstNode node) {
80117
var enclosingFunctionBody = node.thisOrAncestorOfType<FunctionBody>();
81118
return enclosingFunctionBody?.isAsynchronous ?? false;
@@ -99,6 +136,10 @@ class _Visitor extends SimpleAstVisitor<void> {
99136
_isMapClass(expr.methodName.element?.enclosingElement2);
100137

101138
void _visit(Expression expr) {
139+
if (_isAwaitNotRequired(expr)) {
140+
return;
141+
}
142+
102143
// TODO(srawlins): Check whether `expr`'s static type _implements_ `Future`.
103144
if ((expr.staticType?.isDartAsyncFuture ?? false) &&
104145
_isEnclosedInAsyncFunctionBody(expr) &&
@@ -107,3 +148,9 @@ class _Visitor extends SimpleAstVisitor<void> {
107148
}
108149
}
109150
}
151+
152+
extension on Element2? {
153+
bool get hasAwaitNotRequired =>
154+
this is Annotatable &&
155+
(this! as Annotatable).metadata2.hasAwaitNotRequired;
156+
}

pkg/linter/test/rules/unawaited_futures_test.dart

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ void main() {
1414

1515
@reflectiveTest
1616
class UnawaitedFuturesTest extends LintRuleTest {
17+
@override
18+
bool get addMetaPackageDep => true;
19+
1720
@override
1821
String get lintRule => LintNames.unawaited_futures;
1922

@@ -31,6 +34,19 @@ class C {
3134
);
3235
}
3336

37+
test_binaryExpression_unawaited_awaitNotRequired() async {
38+
await assertNoDiagnostics(r'''
39+
import 'package:meta/meta.dart';
40+
void f(C a, C b) async {
41+
a + b;
42+
}
43+
class C {
44+
@awaitNotRequired
45+
Future<int> operator +(C other) async => 7;
46+
}
47+
''');
48+
}
49+
3450
test_boundToFuture_unawaited() async {
3551
// This behavior was not necessarily designed, but this test documents the
3652
// current behavior.
@@ -96,6 +112,17 @@ Future<int> g() => Future.value(0);
96112
);
97113
}
98114

115+
test_functionCall_interpolated_unawaited_awaitNotRequired() async {
116+
await assertNoDiagnostics(r'''
117+
import 'package:meta/meta.dart';
118+
void f() async {
119+
'${g()}';
120+
}
121+
@awaitNotRequired
122+
Future<int> g() => Future.value(0);
123+
''');
124+
}
125+
99126
test_functionCall_nullableFuture_unawaited() async {
100127
await assertDiagnostics(
101128
r'''
@@ -129,19 +156,18 @@ Future<int> g() => Future.value(0);
129156
);
130157
}
131158

132-
test_functionCallInCascade_assignment() async {
159+
test_functionCall_unawaited_awaitNotRequired() async {
133160
await assertNoDiagnostics(r'''
161+
import 'package:meta/meta.dart';
134162
void f() async {
135-
C()..futureField = g();
163+
g();
136164
}
165+
@awaitNotRequired
137166
Future<int> g() => Future.value(0);
138-
class C {
139-
Future<int>? futureField;
140-
}
141167
''');
142168
}
143169

144-
test_functionCallInCascade_inAsync() async {
170+
test_functionCallInCascade() async {
145171
await assertDiagnostics(
146172
r'''
147173
void f() async {
@@ -155,6 +181,33 @@ class C {
155181
);
156182
}
157183

184+
// TODO(srawlins): Test that `@awaitNotRequired` is inherited.
185+
186+
test_functionCallInCascade_assignment() async {
187+
await assertNoDiagnostics(r'''
188+
void f() async {
189+
C()..futureField = g();
190+
}
191+
Future<int> g() => Future.value(0);
192+
class C {
193+
Future<int>? futureField;
194+
}
195+
''');
196+
}
197+
198+
test_functionCallInCascade_awaitNotRequired() async {
199+
await assertNoDiagnostics(r'''
200+
import 'package:meta/meta.dart';
201+
void f() async {
202+
C()..doAsync();
203+
}
204+
class C {
205+
@awaitNotRequired
206+
Future<void> doAsync() async {}
207+
}
208+
''');
209+
}
210+
158211
test_functionCallInCascade_indexAssignment() async {
159212
await assertNoDiagnostics(r'''
160213
void f() async {
@@ -180,6 +233,19 @@ class C {
180233
}
181234

182235
test_instanceProperty_unawaited() async {
236+
await assertNoDiagnostics(r'''
237+
import 'package:meta/meta.dart';
238+
void f(C c) async {
239+
c.p;
240+
}
241+
abstract class C {
242+
@awaitNotRequired
243+
Future<int> get p;
244+
}
245+
''');
246+
}
247+
248+
test_instanceProperty_unawaited_awaitNotRequired() async {
183249
await assertDiagnostics(
184250
r'''
185251
void f(C c) async {
@@ -204,7 +270,7 @@ void f(Future<int> p) async {
204270
);
205271
}
206272

207-
test_unaryExpression_unawaited() async {
273+
test_prefixExpression_unawaited() async {
208274
await assertDiagnostics(
209275
r'''
210276
void f(C a) async {
@@ -218,6 +284,19 @@ class C {
218284
);
219285
}
220286

287+
test_prefixExpression_unawaited_awaitNotRequired() async {
288+
await assertNoDiagnostics(r'''
289+
import 'package:meta/meta.dart';
290+
void f(C a) async {
291+
-a;
292+
}
293+
class C {
294+
@awaitNotRequired
295+
Future<int> operator -() async => 7;
296+
}
297+
''');
298+
}
299+
221300
test_undefinedIdentifier() async {
222301
await assertDiagnostics(
223302
r'''

0 commit comments

Comments
 (0)