no-unnecessary-condition.js 22.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
"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;
                }
                // Predicate is always necessary if it involves `any` or `unknown`
                if (returnTypes.some(t => util_1.isTypeAnyType(t) || 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 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