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);
+}