"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (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 utils_1 = require("@typescript-eslint/utils"); const ts = __importStar(require("typescript")); const tsutils_1 = require("tsutils"); const util_1 = require("../util"); // Truthiness utilities // #region const isTruthyLiteral = (type) => (0, tsutils_1.isBooleanLiteralType)(type, true) || ((0, tsutils_1.isLiteralType)(type) && !!type.value); const isPossiblyFalsy = (type) => (0, tsutils_1.unionTypeParts)(type) // PossiblyFalsy flag includes literal values, so exclude ones that // are definitely truthy .filter(t => !isTruthyLiteral(t)) .some(type => (0, util_1.isTypeFlagSet)(type, ts.TypeFlags.PossiblyFalsy)); const isPossiblyTruthy = (type) => (0, tsutils_1.unionTypeParts)(type).some(type => !(0, tsutils_1.isFalsyType)(type)); // Nullish utilities const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; const isNullishType = (type) => (0, util_1.isTypeFlagSet)(type, nullishFlag); const isPossiblyNullish = (type) => (0, tsutils_1.unionTypeParts)(type).some(isNullishType); const isAlwaysNullish = (type) => (0, tsutils_1.unionTypeParts)(type).every(isNullishType); // isLiteralType only covers numbers and strings, this is a more exhaustive check. const isLiteral = (type) => (0, tsutils_1.isBooleanLiteralType)(type, true) || (0, tsutils_1.isBooleanLiteralType)(type, false) || type.flags === ts.TypeFlags.Undefined || type.flags === ts.TypeFlags.Null || type.flags === ts.TypeFlags.Void || (0, tsutils_1.isLiteralType)(type); exports.default = (0, util_1.createRule)({ name: 'no-unnecessary-condition', meta: { type: 'suggestion', docs: { description: 'Disallow conditionals where the type is always truthy or always falsy', recommended: 'strict', 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 = (0, util_1.getParserServices)(context); const checker = service.program.getTypeChecker(); const sourceCode = context.getSourceCode(); const compilerOptions = service.program.getCompilerOptions(); const isStrictNullChecks = (0, 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 (0, 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 === 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 !== 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 === 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 === 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 ((0, tsutils_1.unionTypeParts)(type).some(part => (0, util_1.isTypeAnyType)(part) || (0, util_1.isTypeUnknownType)(part) || (0, util_1.isTypeFlagSet)(part, ts.TypeFlags.TypeParameter))) { return; } let messageId = null; if ((0, 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) { const type = getNodeType(node); // Conditional is always necessary if it involves `any` or `unknown` if ((0, util_1.isTypeAnyType)(type) || (0, util_1.isTypeUnknownType)(type)) { return; } let messageId = null; if ((0, util_1.isTypeFlagSet)(type, ts.TypeFlags.Never)) { messageId = 'never'; } else if (!isPossiblyNullish(type)) { // 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) && !(node.type === utils_1.AST_NODE_TYPES.ChainExpression && node.expression.type !== utils_1.AST_NODE_TYPES.TSNonNullExpression && optionChainContainsOptionArrayIndex(node.expression))) { 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 (0, 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 && (0, 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 === utils_1.AST_NODE_TYPES.MemberExpression && callee.property.type === 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 === utils_1.AST_NODE_TYPES.ArrowFunctionExpression || callback.type === 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 !== utils_1.AST_NODE_TYPES.BlockStatement) { return checkNode(callback.body); } // () => { return something; } const callbackBody = callback.body.body; if (callbackBody.length === 1 && callbackBody[0].type === 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 = (0, tsutils_1.getCallSignaturesOfType)(getNodeType(callback)).map(sig => sig.getReturnType()); /* istanbul ignore if */ if (returnTypes.length === 0) { // Not a callable function return; } // Predicate is always necessary if it involves `any` or `unknown` if (returnTypes.some(t => (0, util_1.isTypeAnyType)(t) || (0, util_1.isTypeUnknownType)(t))) { 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 optionChainContainsOptionArrayIndex(node) { const lhsNode = node.type === utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object; if (node.optional && isArrayIndexExpression(lhsNode)) { return true; } if (lhsNode.type === utils_1.AST_NODE_TYPES.MemberExpression || lhsNode.type === utils_1.AST_NODE_TYPES.CallExpression) { return optionChainContainsOptionArrayIndex(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 = (0, util_1.getTypeOfPropertyOfName)(checker, objType, propertyType.value.toString()); if (propType) { return (0, util_1.isNullableType)(propType, { allowUndefined: true }); } } const typeName = (0, 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() && (0, util_1.isIdentifier)(property)) { const isOwnNullable = prevType.types.some(type => { if (node.computed) { const propertyType = getNodeType(node.property); return isNullablePropertyType(type, propertyType); } const propType = (0, util_1.getTypeOfPropertyOfName)(checker, type, property.name); return propType && (0, util_1.isNullableType)(propType, { allowUndefined: true }); }); return (!isOwnNullable && (0, util_1.isNullableType)(prevType, { allowUndefined: true })); } return false; } function isOptionableExpression(node) { const type = getNodeType(node); const isOwnNullable = node.type === utils_1.AST_NODE_TYPES.MemberExpression ? !isNullableOriginFromPrev(node) : true; return ((0, util_1.isTypeAnyType)(type) || (0, util_1.isTypeUnknownType)(type) || ((0, 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 (optionChainContainsOptionArrayIndex(node)) { return; } const nodeToCheck = node.type === utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object; if (isOptionableExpression(nodeToCheck)) { return; } const questionDotOperator = (0, util_1.nullThrows)(sourceCode.getTokenAfter(beforeOperator, token => token.type === 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