| "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 |