Ban using "toBeTruthy()" to match Promises, which is error-prone. Promises are always truthy - it's likely that instead the developer intended to match the result of the Promise.

PiperOrigin-RevId: 184560412
diff --git a/docs/ban-expect-truthy-promise.md b/docs/ban-expect-truthy-promise.md
new file mode 100644
index 0000000..47ba723
--- /dev/null
+++ b/docs/ban-expect-truthy-promise.md
@@ -0,0 +1,33 @@
+<!-- FIXME(alexeagle): generate the docs from the sources -->
+
+## Don't expect promises toBeTruthy()
+
+Don't write expectations like this:
+
+    expect(returnsPromise()).toBeTruthy();
+
+Promises are always truthy, so this assertion will never fail. Usually, the
+intention was to match the result of the promise. If that's the case, simply
+add an `await`.
+
+    expect(await returnsPromise()).toBeTruthy();
+
+### In Protractor tests
+
+If you're not writing a Protractor test, you can safely ignore this section.
+
+In the past, Protractor tests have patched `expect()` to automatically unwrap
+promises, which made these assertions work as expected without needing an
+`await`.  However, the [control flow is deprecated][1] and will be removed in
+Selenium WebDriver 4.0, and these assertions are now a bug. You'll
+need to `await` the promise as above.
+
+If you can't `await` the promise because your tests need the control flow, you
+can use a more specific matcher.
+
+    expect(returnsPromise()).toBe(true);
+
+This assertion will work as expected for now and will fail when the control flow
+is finally removed.
+
+[1]: https://github.com/SeleniumHQ/selenium/issues/2969
diff --git a/internal/tsc_wrapped/package.json b/internal/tsc_wrapped/package.json
index 2149c53..c30b905 100644
--- a/internal/tsc_wrapped/package.json
+++ b/internal/tsc_wrapped/package.json
@@ -7,8 +7,8 @@
     "@types/tmp": "0.0.33",
     "protobufjs": "5.0.0",
     "tsickle": "0.25.5",
-    "typescript": "2.5.3",
-    "tsutils": "2.12.1",
+    "typescript": "2.6.2",
+    "tsutils": "2.20.0",
     "tmp": "0.0.33"
   }
 }
diff --git a/internal/tsetse/error_code.ts b/internal/tsetse/error_code.ts
index ef1df60..321a716 100644
--- a/internal/tsetse/error_code.ts
+++ b/internal/tsetse/error_code.ts
@@ -1,4 +1,5 @@
 export enum ErrorCode {
   CHECK_RETURN_VALUE = 21222,
   EQUALS_NAN = 21223,
+  BAN_EXPECT_TRUTHY_PROMISE = 21224,
 }
diff --git a/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts b/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts
new file mode 100644
index 0000000..6f852ae
--- /dev/null
+++ b/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts
@@ -0,0 +1,77 @@
+/**
+ * @fileoverview Bans expect(returnsPromise()).toBeTruthy(). Promises are always
+ * truthy, and this pattern is likely to be a bug where the developer meant
+ * expect(await returnsPromise()).toBeTruthy() and forgot the await.
+ */
+
+import * as tsutils from 'tsutils';
+import * as ts from 'typescript';
+
+import {Checker} from '../checker';
+import {ErrorCode} from '../error_code';
+import {AbstractRule} from '../rule';
+
+export class Rule extends AbstractRule {
+  readonly ruleName = 'ban-expect-truthy-promise';
+  readonly code = ErrorCode.BAN_EXPECT_TRUTHY_PROMISE;
+
+  register(checker: Checker) {
+    checker.on(
+        ts.SyntaxKind.PropertyAccessExpression, checkForTruthy, this.code);
+  }
+}
+
+function checkForTruthy(checker: Checker, node: ts.Node) {
+  const tc = checker.typeChecker;
+
+  if (!tsutils.isPropertyAccessExpression(node)) {
+    return;
+  }
+
+  if (node.name.getText() !== 'toBeTruthy') {
+    return;
+  }
+
+  const expectCallNode = getLeftmostNode(tc, node);
+  if (!ts.isCallExpression(expectCallNode)) {
+    return;
+  }
+
+  const signature = checker.typeChecker.getResolvedSignature(expectCallNode);
+  if (signature === undefined) {
+    return;
+  }
+
+  const symbol = tc.getReturnTypeOfSignature(signature).getSymbol();
+  if (symbol === undefined) {
+    return;
+  }
+
+  // Only look for methods named expect that return a Matchers
+  if (!((symbol.name === 'Matchers') &&
+        expectCallNode.expression.getText() === 'expect')) {
+    return;
+  }
+
+  if (!tsutils.isThenableType(tc, expectCallNode.arguments[0]) ||
+      tsutils.isAwaitExpression(expectCallNode.arguments[0])) {
+    return;
+  }
+
+  const argType = tc.getTypeAtLocation(expectCallNode.arguments[0]);
+  checker.addFailureAtNode(
+      node,
+      `Value passed to expect() is of type ${tc.typeToString(argType)}, which` +
+          ` is thenable. Promises are always truthy. Either use toBe(true) or` +
+          ` await the value.` +
+          `\n\tSee http://tsetse.info/ban-expect-truthy-promise`);
+}
+
+function getLeftmostNode(
+    tc: ts.TypeChecker, node: ts.PropertyAccessExpression) {
+  let current: ts.LeftHandSideExpression|undefined = node;
+  while (ts.isPropertyAccessExpression(current)) {
+    current = current.expression;
+  }
+  return current;
+}
diff --git a/internal/tsetse/runner.ts b/internal/tsetse/runner.ts
index c046dc7..2d77c21 100644
--- a/internal/tsetse/runner.ts
+++ b/internal/tsetse/runner.ts
@@ -9,6 +9,7 @@
 
 import {Checker} from './checker';
 import {AbstractRule} from './rule';
+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';
 
@@ -19,6 +20,7 @@
 const ENABLED_RULES: AbstractRule[] = [
   new CheckReturnValueRule(),
   new EqualsNanRule(),
+  new BanExpectTruthyPromiseRule(),
 ];
 
 /**
diff --git a/internal/tsetse/tests/ban_expect_truthy_promise/jasmine_types.ts b/internal/tsetse/tests/ban_expect_truthy_promise/jasmine_types.ts
new file mode 100644
index 0000000..bcea24f
--- /dev/null
+++ b/internal/tsetse/tests/ban_expect_truthy_promise/jasmine_types.ts
@@ -0,0 +1,14 @@
+// Abbreviated Jasmine types for testing purposes
+
+declare function describe(
+    description: string, specDefinitions: () => void): void;
+declare function it(
+    expectation: string, assertion?: (done: Function) => void,
+    timeout?: number): void;
+declare function expect(actual: any): Matchers;
+
+interface Matchers {
+  toBeTruthy(expectationFailOutput?: any): boolean;
+  toBeFalsy(expectationFailOutput?: any): boolean;
+  not: Matchers;
+}
diff --git a/internal/tsetse/tests/ban_expect_truthy_promise/negatives.ts b/internal/tsetse/tests/ban_expect_truthy_promise/negatives.ts
new file mode 100644
index 0000000..62a3485
--- /dev/null
+++ b/internal/tsetse/tests/ban_expect_truthy_promise/negatives.ts
@@ -0,0 +1,10 @@
+function expect(x: {}): ClassWithTruthy {
+  return new ClassWithTruthy();
+}
+
+class ClassWithTruthy {
+  toBeTruthy() {}
+}
+
+new ClassWithTruthy().toBeTruthy();
+expect(Promise.resolve(1)).toBeTruthy();
diff --git a/internal/tsetse/tests/ban_expect_truthy_promise/positives.ts b/internal/tsetse/tests/ban_expect_truthy_promise/positives.ts
new file mode 100644
index 0000000..6ec611f
--- /dev/null
+++ b/internal/tsetse/tests/ban_expect_truthy_promise/positives.ts
@@ -0,0 +1,53 @@
+function returnsPromise() {
+  return Promise.resolve(true);
+}
+
+type Future = Promise<number>;
+function aliasedPromise(): Future {
+  return Promise.resolve(1);
+}
+
+class Extended extends Promise<number> {}
+function extendedPromise(): Extended {
+  return Promise.resolve(1);
+}
+
+class DoubleExtended extends Extended {}
+function doubleExtendedPromise(): Extended {
+  return Promise.resolve(1);
+}
+
+function maybePromise(): Promise<number>|number {
+  return 3;
+}
+
+describe('example test', () => {
+  it('should error when expecting promises toBeTruthy', async () => {
+    const promise = returnsPromise();
+
+    // Every expect here should fail the check.
+    expect(promise).toBeTruthy();
+    expect(maybePromise()).not.toBeTruthy();
+    expect(maybePromise()).toBeTruthy();
+    expect(returnsPromise()).toBeTruthy();
+    expect(aliasedPromise()).toBeTruthy();
+    expect(extendedPromise()).toBeTruthy();
+    expect(doubleExtendedPromise()).toBeTruthy();
+  });
+
+  it('should allow awaited promises', async () => {
+    const promise = returnsPromise();
+    expect(await promise).toBeTruthy();
+    expect(await maybePromise()).toBeTruthy();
+    expect(await returnsPromise()).toBeTruthy();
+    expect(await aliasedPromise()).toBeTruthy();
+    expect(await extendedPromise()).toBeTruthy();
+  });
+
+  it('should only error for promises', async () => {
+    expect('truthy').toBeTruthy();
+    expect(1).toBeTruthy();
+    expect(true).toBeTruthy();
+    expect(null).not.toBeTruthy();
+  });
+});
diff --git a/internal/tsetse/tsconfig.json b/internal/tsetse/tsconfig.json
index a5f8cb1..e59aeac 100644
--- a/internal/tsetse/tsconfig.json
+++ b/internal/tsetse/tsconfig.json
@@ -9,7 +9,8 @@
       "dom",
       "es5",
       "es2015.core",
-      "es2015.collection"
+      "es2015.collection",
+      "es2015.promise"
     ],
     "plugins": [
       // Run `bazel build internal/tsetse:language_service_plugin` for this to work.
diff --git a/package.json b/package.json
index b973398..41a0693 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,8 @@
         "protobufjs": "5.0.0",
         "protractor": "^5.2.0",
         "tsickle": "0.25.x",
-        "tsutils": "2.12.1",
-        "typescript": "2.5.x"
+        "tsutils": "2.20.0",
+        "typescript": "2.6.x"
     },
     "scripts": {
         "pretest": "webdriver-manager update && bazel build examples/app:all",