blob: 32b71398b398e04fd7b9ad770c5dc2aa8af51488 [file] [log] [blame]
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const experimental_utils_1 = require("@typescript-eslint/experimental-utils");
const ts = __importStar(require("typescript"));
const tsutils_1 = require("tsutils");
const util_1 = require("../util");
// Truthiness utilities
// #region
const isTruthyLiteral = (type) => tsutils_1.isBooleanLiteralType(type, true) || (tsutils_1.isLiteralType(type) && !!type.value);
const isPossiblyFalsy = (type) => tsutils_1.unionTypeParts(type)
// PossiblyFalsy flag includes literal values, so exclude ones that
// are definitely truthy
.filter(t => !isTruthyLiteral(t))
.some(type => util_1.isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy));
const isPossiblyTruthy = (type) => tsutils_1.unionTypeParts(type).some(type => !tsutils_1.isFalsyType(type));
// Nullish utilities
const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
const isNullishType = (type) => util_1.isTypeFlagSet(type, nullishFlag);
const isPossiblyNullish = (type) => tsutils_1.unionTypeParts(type).some(isNullishType);
const isAlwaysNullish = (type) => tsutils_1.unionTypeParts(type).every(isNullishType);
// isLiteralType only covers numbers and strings, this is a more exhaustive check.
const isLiteral = (type) => tsutils_1.isBooleanLiteralType(type, true) ||
tsutils_1.isBooleanLiteralType(type, false) ||
type.flags === ts.TypeFlags.Undefined ||
type.flags === ts.TypeFlags.Null ||
type.flags === ts.TypeFlags.Void ||
tsutils_1.isLiteralType(type);
exports.default = util_1.createRule({
name: 'no-unnecessary-condition',
meta: {
type: 'suggestion',
docs: {
description: 'Prevents conditionals where the type is always truthy or always falsy',
category: 'Best Practices',
recommended: false,
requiresTypeChecking: true,
},
schema: [
{
type: 'object',
properties: {
allowConstantLoopConditions: {
type: 'boolean',
},
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
fixable: 'code',
messages: {
alwaysTruthy: 'Unnecessary conditional, value is always truthy.',
alwaysFalsy: 'Unnecessary conditional, value is always falsy.',
alwaysTruthyFunc: 'This callback should return a conditional, but return is always truthy.',
alwaysFalsyFunc: 'This callback should return a conditional, but return is always falsy.',
neverNullish: 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.',
alwaysNullish: 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.',
literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values',
noOverlapBooleanExpression: 'Unnecessary conditional, the types have no overlap',
never: 'Unnecessary conditional, value is `never`',
neverOptionalChain: 'Unnecessary optional chain on a non-nullish value',
noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
},
},
defaultOptions: [
{
allowConstantLoopConditions: false,
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
},
],
create(context, [{ allowConstantLoopConditions, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, },]) {
const service = util_1.getParserServices(context);
const checker = service.program.getTypeChecker();
const sourceCode = context.getSourceCode();
const compilerOptions = service.program.getCompilerOptions();
const isStrictNullChecks = tsutils_1.isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks');
if (!isStrictNullChecks &&
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true) {
context.report({
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
},
messageId: 'noStrictNullCheck',
});
}
function getNodeType(node) {
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
return util_1.getConstrainedTypeAtLocation(checker, tsNode);
}
function nodeIsArrayType(node) {
const nodeType = getNodeType(node);
return checker.isArrayType(nodeType);
}
function nodeIsTupleType(node) {
const nodeType = getNodeType(node);
return checker.isTupleType(nodeType);
}
function isArrayIndexExpression(node) {
return (
// Is an index signature
node.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression &&
node.computed &&
// ...into an array type
(nodeIsArrayType(node.object) ||
// ... or a tuple type
(nodeIsTupleType(node.object) &&
// Exception: literal index into a tuple - will have a sound type
node.property.type !== experimental_utils_1.AST_NODE_TYPES.Literal)));
}
/**
* Checks if a conditional node is necessary:
* if the type of the node is always true or always false, it's not necessary.
*/
function checkNode(node, isUnaryNotArgument = false) {
// Check if the node is Unary Negation expression and handle it
if (node.type === experimental_utils_1.AST_NODE_TYPES.UnaryExpression &&
node.operator === '!') {
return checkNode(node.argument, true);
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (isArrayIndexExpression(node)) {
return;
}
// When checking logical expressions, only check the right side
// as the left side has been checked by checkLogicalExpressionForUnnecessaryConditionals
//
// Unless the node is nullish coalescing, as it's common to use patterns like `nullBool ?? true` to to strict
// boolean checks if we inspect the right here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
if (node.type === experimental_utils_1.AST_NODE_TYPES.LogicalExpression &&
node.operator !== '??') {
return checkNode(node.right);
}
const type = getNodeType(node);
// Conditional is always necessary if it involves:
// `any` or `unknown` or a naked type parameter
if (tsutils_1.unionTypeParts(type).some(part => util_1.isTypeAnyType(part) ||
util_1.isTypeUnknownType(part) ||
util_1.isTypeFlagSet(part, ts.TypeFlags.TypeParameter))) {
return;
}
let messageId = null;
if (util_1.isTypeFlagSet(type, ts.TypeFlags.Never)) {
messageId = 'never';
}
else if (!isPossiblyTruthy(type)) {
messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
}
else if (!isPossiblyFalsy(type)) {
messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
}
if (messageId) {
context.report({ node, messageId });
}
}
function checkNodeForNullish(node) {
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (isArrayIndexExpression(node)) {
return;
}
const type = getNodeType(node);
// Conditional is always necessary if it involves `any` or `unknown`
if (util_1.isTypeAnyType(type) || util_1.isTypeUnknownType(type)) {
return;
}
let messageId = null;
if (util_1.isTypeFlagSet(type, ts.TypeFlags.Never)) {
messageId = 'never';
}
else if (!isPossiblyNullish(type)) {
messageId = 'neverNullish';
}
else if (isAlwaysNullish(type)) {
messageId = 'alwaysNullish';
}
if (messageId) {
context.report({ node, messageId });
}
}
/**
* Checks that a binary expression is necessarily conditional, reports otherwise.
* If both sides of the binary expression are literal values, it's not a necessary condition.
*
* NOTE: It's also unnecessary if the types that don't overlap at all
* but that case is handled by the Typescript compiler itself.
* Known exceptions:
* * https://github.com/microsoft/TypeScript/issues/32627
* * https://github.com/microsoft/TypeScript/issues/37160 (handled)
*/
const BOOL_OPERATORS = new Set([
'<',
'>',
'<=',
'>=',
'==',
'===',
'!=',
'!==',
]);
function checkIfBinaryExpressionIsNecessaryConditional(node) {
if (!BOOL_OPERATORS.has(node.operator)) {
return;
}
const leftType = getNodeType(node.left);
const rightType = getNodeType(node.right);
if (isLiteral(leftType) && isLiteral(rightType)) {
context.report({ node, messageId: 'literalBooleanExpression' });
return;
}
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
if (isStrictNullChecks) {
const UNDEFINED = ts.TypeFlags.Undefined;
const NULL = ts.TypeFlags.Null;
const isComparable = (type, flag) => {
// Allow comparison to `any`, `unknown` or a naked type parameter.
flag |=
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.TypeParameter;
// Allow loose comparison to nullish values.
if (node.operator === '==' || node.operator === '!=') {
flag |= NULL | UNDEFINED;
}
return util_1.isTypeFlagSet(type, flag);
};
if ((leftType.flags === UNDEFINED &&
!isComparable(rightType, UNDEFINED)) ||
(rightType.flags === UNDEFINED &&
!isComparable(leftType, UNDEFINED)) ||
(leftType.flags === NULL && !isComparable(rightType, NULL)) ||
(rightType.flags === NULL && !isComparable(leftType, NULL))) {
context.report({ node, messageId: 'noOverlapBooleanExpression' });
return;
}
}
}
/**
* Checks that a logical expression contains a boolean, reports otherwise.
*/
function checkLogicalExpressionForUnnecessaryConditionals(node) {
if (node.operator === '??') {
checkNodeForNullish(node.left);
return;
}
// Only checks the left side, since the right side might not be "conditional" at all.
// The right side will be checked if the LogicalExpression is used in a conditional context
checkNode(node.left);
}
/**
* Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
*/
function checkIfLoopIsNecessaryConditional(node) {
if (node.test === null) {
// e.g. `for(;;)`
return;
}
/**
* Allow:
* while (true) {}
* for (;true;) {}
* do {} while (true)
*/
if (allowConstantLoopConditions &&
tsutils_1.isBooleanLiteralType(getNodeType(node.test), true)) {
return;
}
checkNode(node.test);
}
const ARRAY_PREDICATE_FUNCTIONS = new Set([
'filter',
'find',
'some',
'every',
]);
function isArrayPredicateFunction(node) {
const { callee } = node;
return (
// looks like `something.filter` or `something.find`
callee.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression &&
callee.property.type === experimental_utils_1.AST_NODE_TYPES.Identifier &&
ARRAY_PREDICATE_FUNCTIONS.has(callee.property.name) &&
// and the left-hand side is an array, according to the types
(nodeIsArrayType(callee.object) || nodeIsTupleType(callee.object)));
}
function checkCallExpression(node) {
// If this is something like arr.filter(x => /*condition*/), check `condition`
if (isArrayPredicateFunction(node) && node.arguments.length) {
const callback = node.arguments[0];
// Inline defined functions
if ((callback.type === experimental_utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
callback.type === experimental_utils_1.AST_NODE_TYPES.FunctionExpression) &&
callback.body) {
// Two special cases, where we can directly check the node that's returned:
// () => something
if (callback.body.type !== experimental_utils_1.AST_NODE_TYPES.BlockStatement) {
return checkNode(callback.body);
}
// () => { return something; }
const callbackBody = callback.body.body;
if (callbackBody.length === 1 &&
callbackBody[0].type === experimental_utils_1.AST_NODE_TYPES.ReturnStatement &&
callbackBody[0].argument) {
return checkNode(callbackBody[0].argument);
}
// Potential enhancement: could use code-path analysis to check
// any function with a single return statement
// (Value to complexity ratio is dubious however)
}
// Otherwise just do type analysis on the function as a whole.
const returnTypes = tsutils_1.getCallSignaturesOfType(getNodeType(callback)).map(sig => sig.getReturnType());
/* istanbul ignore if */ if (returnTypes.length === 0) {
// Not a callable function
return;
}
if (!returnTypes.some(isPossiblyFalsy)) {
return context.report({
node: callback,
messageId: 'alwaysTruthyFunc',
});
}
if (!returnTypes.some(isPossiblyTruthy)) {
return context.report({
node: callback,
messageId: 'alwaysFalsyFunc',
});
}
}
}
// Recursively searches an optional chain for an array index expression
// Has to search the entire chain, because an array index will "infect" the rest of the types
// Example:
// ```
// [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
// ?.x // type is {y: "z"}
// ?.y // This access is considered "unnecessary" according to the types
// ```
function optionChainContainsArrayIndex(node) {
const lhsNode = node.type === experimental_utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (isArrayIndexExpression(lhsNode)) {
return true;
}
if (lhsNode.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression ||
lhsNode.type === experimental_utils_1.AST_NODE_TYPES.CallExpression) {
return optionChainContainsArrayIndex(lhsNode);
}
return false;
}
function isNullablePropertyType(objType, propertyType) {
if (propertyType.isUnion()) {
return propertyType.types.some(type => isNullablePropertyType(objType, type));
}
if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
const propType = util_1.getTypeOfPropertyOfName(checker, objType, propertyType.value.toString());
if (propType) {
return util_1.isNullableType(propType, { allowUndefined: true });
}
}
const typeName = util_1.getTypeName(checker, propertyType);
return !!((typeName === 'string' &&
checker.getIndexInfoOfType(objType, ts.IndexKind.String)) ||
(typeName === 'number' &&
checker.getIndexInfoOfType(objType, ts.IndexKind.Number)));
}
// Checks whether a member expression is nullable or not regardless of it's previous node.
// Example:
// ```
// // 'bar' is nullable if 'foo' is null.
// // but this function checks regardless of 'foo' type, so returns 'true'.
// declare const foo: { bar : { baz: string } } | null
// foo?.bar;
// ```
function isNullableOriginFromPrev(node) {
const prevType = getNodeType(node.object);
const property = node.property;
if (prevType.isUnion() && util_1.isIdentifier(property)) {
const isOwnNullable = prevType.types.some(type => {
if (node.computed) {
const propertyType = getNodeType(node.property);
return isNullablePropertyType(type, propertyType);
}
const propType = util_1.getTypeOfPropertyOfName(checker, type, property.name);
return propType && util_1.isNullableType(propType, { allowUndefined: true });
});
return (!isOwnNullable && util_1.isNullableType(prevType, { allowUndefined: true }));
}
return false;
}
function isOptionableExpression(node) {
const type = getNodeType(node);
const isOwnNullable = node.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression
? !isNullableOriginFromPrev(node)
: true;
return (util_1.isTypeAnyType(type) ||
util_1.isTypeUnknownType(type) ||
(util_1.isNullableType(type, { allowUndefined: true }) && isOwnNullable));
}
function checkOptionalChain(node, beforeOperator, fix) {
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
if (!node.optional) {
return;
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (optionChainContainsArrayIndex(node)) {
return;
}
const nodeToCheck = node.type === experimental_utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (isOptionableExpression(nodeToCheck)) {
return;
}
const questionDotOperator = util_1.nullThrows(sourceCode.getTokenAfter(beforeOperator, token => token.type === experimental_utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '?.'), util_1.NullThrowsReasons.MissingToken('operator', node.type));
context.report({
node,
loc: questionDotOperator.loc,
messageId: 'neverOptionalChain',
fix(fixer) {
return fixer.replaceText(questionDotOperator, fix);
},
});
}
function checkOptionalMemberExpression(node) {
checkOptionalChain(node, node.object, node.computed ? '' : '.');
}
function checkOptionalCallExpression(node) {
checkOptionalChain(node, node.callee, '');
}
return {
BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional,
CallExpression: checkCallExpression,
ConditionalExpression: (node) => checkNode(node.test),
DoWhileStatement: checkIfLoopIsNecessaryConditional,
ForStatement: checkIfLoopIsNecessaryConditional,
IfStatement: (node) => checkNode(node.test),
LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
WhileStatement: checkIfLoopIsNecessaryConditional,
'MemberExpression[optional = true]': checkOptionalMemberExpression,
'CallExpression[optional = true]': checkOptionalCallExpression,
};
},
});
//# sourceMappingURL=no-unnecessary-condition.js.map