Require all promises in an async function to be consumed in some way (awaited, returned, assigned to a variable, etc). Not using a promise in an async function is likely to be a bug.
PiperOrigin-RevId: 185670908
diff --git a/docs/must-use-promises.md b/docs/must-use-promises.md
new file mode 100644
index 0000000..97c5bae
--- /dev/null
+++ b/docs/must-use-promises.md
@@ -0,0 +1,21 @@
+<!-- FIXME(alexeagle): generate the docs from the sources -->
+
+## All promises in async functions must be used
+
+When using `async` / `await`, it can be easy to forget to `await` a promise
+which can result in async tasks running in an unexpected order. Thus, we require
+that every promise in an `async` function is consumed in some way, so that
+there's a well-defined order in which async tasks are resolved. To fix this
+check, you can do one of the following:
+
+1. Remove `async` from the function, and instead use normal promise chaining.
+2. `await` the promise.
+3. Assign the promise to a variable and `await` it later.
+
+As a last resort, if you really want to use `async` and can't `await` the promise,
+you can just assign it to a variable, like so
+
+ let ignoredPromise = returnsPromise();
+
+This makes your intent to ignore the result of the promise explicit, although it
+may cause "declared but never used" warning with some linters.
diff --git a/internal/tsetse/error_code.ts b/internal/tsetse/error_code.ts
index 321a716..99519a1 100644
--- a/internal/tsetse/error_code.ts
+++ b/internal/tsetse/error_code.ts
@@ -2,4 +2,5 @@
CHECK_RETURN_VALUE = 21222,
EQUALS_NAN = 21223,
BAN_EXPECT_TRUTHY_PROMISE = 21224,
+ MUST_USE_PROMISES = 21225,
}
diff --git a/internal/tsetse/rules/must_use_promises_rule.ts b/internal/tsetse/rules/must_use_promises_rule.ts
new file mode 100644
index 0000000..eab2f63
--- /dev/null
+++ b/internal/tsetse/rules/must_use_promises_rule.ts
@@ -0,0 +1,58 @@
+/**
+ * @fileoverview A Tsetse rule that checks that all promises in async function
+ * blocks are awaited or used.
+ */
+
+import * as tsutils from 'tsutils';
+import * as ts from 'typescript';
+
+import {Checker} from '../checker';
+import {ErrorCode} from '../error_code';
+import {AbstractRule} from '../rule';
+
+const FAILURE_STRING =
+ 'All Promises in async functions must either be awaited or used in an expression.' +
+ '\n\tSee http://tsetse.info/must-use-promises';
+
+export class Rule extends AbstractRule {
+ readonly ruleName = 'must-use-promises';
+ readonly code = ErrorCode.MUST_USE_PROMISES;
+
+ register(checker: Checker) {
+ checker.on(ts.SyntaxKind.CallExpression, checkCallExpression, this.code);
+ }
+}
+
+function checkCallExpression(checker: Checker, node: ts.CallExpression) {
+ const signature = checker.typeChecker.getResolvedSignature(node);
+ if (signature === undefined) {
+ return;
+ }
+
+ const returnType = checker.typeChecker.getReturnTypeOfSignature(signature);
+ if (!!(returnType.flags & ts.TypeFlags.Void)) {
+ return;
+ }
+
+ if (tsutils.isExpressionValueUsed(node)) {
+ return;
+ }
+
+ if (inAsyncFunction(node) &&
+ tsutils.isThenableType(checker.typeChecker, node)) {
+ checker.addFailureAtNode(node, FAILURE_STRING);
+ }
+}
+
+function inAsyncFunction(node: ts.Node): boolean {
+ const isFunction = tsutils.isFunctionDeclaration(node) ||
+ tsutils.isArrowFunction(node) || tsutils.isMethodDeclaration(node) ||
+ tsutils.isFunctionExpression(node);
+ if (isFunction) {
+ return tsutils.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword);
+ }
+ if (node.parent) {
+ return inAsyncFunction(node.parent);
+ }
+ return false;
+}
diff --git a/internal/tsetse/runner.ts b/internal/tsetse/runner.ts
index 2d77c21..a2f14b3 100644
--- a/internal/tsetse/runner.ts
+++ b/internal/tsetse/runner.ts
@@ -12,6 +12,7 @@
import {Rule as BanExpectTruthyPromiseRule} from './rules/ban_expect_truthy_promise_rule';
import {Rule as CheckReturnValueRule} from './rules/check_return_value_rule';
import {Rule as EqualsNanRule} from './rules/equals_nan_rule';
+import {Rule as MustUsePromisesRule} from './rules/must_use_promises_rule';
/**
* List of Tsetse rules. Shared between the program plugin and the language
@@ -21,6 +22,7 @@
new CheckReturnValueRule(),
new EqualsNanRule(),
new BanExpectTruthyPromiseRule(),
+ new MustUsePromisesRule(),
];
/**
diff --git a/internal/tsetse/tests/must_use_promises/no_await.ts b/internal/tsetse/tests/must_use_promises/no_await.ts
new file mode 100644
index 0000000..d2b9adc
--- /dev/null
+++ b/internal/tsetse/tests/must_use_promises/no_await.ts
@@ -0,0 +1,9 @@
+import {noAwait} from 'google3/javascript/typescript/contrib/async';
+
+function returnsPromise() {
+ return Promise.resolve(1);
+}
+
+async function unusedPromises() {
+ noAwait(returnsPromise());
+}
diff --git a/internal/tsetse/tests/must_use_promises/positives.ts b/internal/tsetse/tests/must_use_promises/positives.ts
new file mode 100644
index 0000000..659802a
--- /dev/null
+++ b/internal/tsetse/tests/must_use_promises/positives.ts
@@ -0,0 +1,54 @@
+function isPromise() {
+ return Promise.resolve(1);
+}
+
+function maybePromise(): Promise<number>|number {
+ return Promise.resolve(1);
+}
+
+type Future = Promise<number>;
+function aliasedPromise(): Future {
+ return Promise.resolve(1);
+}
+
+class Extended extends Promise<number> {}
+
+function extendedPromise(): Extended {
+ return Promise.resolve(1);
+}
+
+function usePromise(maybePromise: Promise<number>|number) {
+ return false;
+}
+
+async function returnLaterPromise() {
+ return () => Promise.resolve(1);
+}
+
+async function unusedPromises() {
+ isPromise();
+ maybePromise();
+ aliasedPromise();
+ extendedPromise();
+ const later = await returnLaterPromise();
+ later();
+}
+
+async function hasAwait() {
+ await isPromise();
+ await maybePromise();
+ await aliasedPromise();
+ await extendedPromise();
+ const later = await returnLaterPromise();
+ await later();
+ usePromise(maybePromise());
+}
+
+function nonAsyncFunction() {
+ isPromise();
+ maybePromise();
+ aliasedPromise();
+ extendedPromise();
+ const future = maybePromise();
+ usePromise(future);
+}