blob: a58461a808b58c014ccf67469e27a7700ffd4e98 [file] [log] [blame]
/**
* @fileoverview Bans using a promise as a condition. Promises are always
* truthy, and this pattern is likely to be a bug where the developer meant
* if(await returnsPromise()) {} 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-promise-as-condition';
readonly code = ErrorCode.BAN_PROMISE_AS_CONDITION;
register(checker: Checker) {
checker.on(
ts.SyntaxKind.ConditionalExpression, checkConditional, this.code);
checker.on(
ts.SyntaxKind.BinaryExpression, checkBinaryExpression, this.code);
checker.on(ts.SyntaxKind.WhileStatement, checkWhileStatement, this.code);
checker.on(ts.SyntaxKind.IfStatement, checkIfStatement, this.code);
}
}
/** Error message to display. */
function thenableText(nodeType: string, isVariable: boolean) {
return `Found a thenable ${isVariable ? 'variable' : 'return value'} being` +
` used as ${
nodeType}. Promises are always truthy, await the value to get` +
' a boolean value.';
}
function thenableVariableText(nodeType: string) {
return thenableText(nodeType, true);
}
function thenableReturnText(nodeType: string) {
return thenableText(nodeType, false);
}
/** Ternary: prom ? y : z */
function checkConditional(checker: Checker, node: ts.ConditionalExpression) {
addFailureIfThenableCallExpression(
checker, node.condition, thenableReturnText('a conditional'));
addFailureIfThenableIdentifier(
checker, node.condition, thenableVariableText('a conditional'));
}
/**
* Binary expression: prom || y or prom && y. Only check left side because
* myThing && myThing.prom seems legitimate.
*/
function checkBinaryExpression(checker: Checker, node: ts.BinaryExpression) {
if (node.operatorToken.kind !== ts.SyntaxKind.BarBarToken &&
node.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) {
return;
}
addFailureIfThenableCallExpression(
checker, node.left, thenableReturnText('a binary expression'));
addFailureIfThenableIdentifier(
checker, node.left, thenableVariableText('a binary expression'));
}
/** While statement: while (prom) {} */
function checkWhileStatement(checker: Checker, node: ts.WhileStatement) {
addFailureIfThenableCallExpression(
checker, node.expression, thenableReturnText('a while statement'));
addFailureIfThenableIdentifier(
checker, node.expression, thenableVariableText('a while statement'));
}
/** If statement: if (prom) {} */
function checkIfStatement(checker: Checker, node: ts.IfStatement) {
addFailureIfThenableCallExpression(
checker, node.expression, thenableReturnText('an if statement'));
addFailureIfThenableIdentifier(
checker, node.expression, thenableVariableText('an if statement'));
}
/** Helper methods */
function addFailureIfThenableCallExpression(
checker: Checker, callExpression: ts.Expression, errorMessage: string) {
if (!tsutils.isCallExpression(callExpression)) {
return;
}
const typeChecker = checker.typeChecker;
const signature = typeChecker.getResolvedSignature(callExpression);
// Return value of getResolvedSignature is `Signature | undefined` in ts 3.1
// so we must check if the return value is valid to compile with ts 3.1.
if (!signature) {
throw new Error('Unexpected undefined signature for call expression');
}
const returnType = typeChecker.getReturnTypeOfSignature(signature);
if (isNonFalsyThenableType(typeChecker, callExpression, returnType)) {
checker.addFailureAtNode(callExpression, errorMessage);
}
}
function addFailureIfThenableIdentifier(
checker: Checker, identifier: ts.Expression, errorMessage: string) {
if (!tsutils.isIdentifier(identifier)) {
return;
}
if (isNonFalsyThenableType(checker.typeChecker, identifier)) {
checker.addFailureAtNode(identifier, errorMessage);
}
}
/**
* If the type is a union type and has a falsy part it may be legitimate to use
* it as a condition, so allow those through. (e.g. Promise<boolean> | boolean)
* Otherwise, check if it's thenable. If so it should be awaited.
*/
function isNonFalsyThenableType(
typeChecker: ts.TypeChecker, node: ts.Expression,
type = typeChecker.getTypeAtLocation(node)) {
if (hasFalsyParts(typeChecker.getTypeAtLocation(node))) {
return false;
}
return tsutils.isThenableType(typeChecker, node, type);
}
function hasFalsyParts(type: ts.Type) {
const typeParts = tsutils.unionTypeParts(type);
const hasFalsyParts =
typeParts.filter((part) => tsutils.isFalsyType(part)).length > 0;
return hasFalsyParts;
}