prefer-includes.js 9.28 KB
"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 regexpp_1 = require("regexpp");
const ts = __importStar(require("typescript"));
const util_1 = require("../util");
exports.default = util_1.createRule({
    name: 'prefer-includes',
    defaultOptions: [],
    meta: {
        type: 'suggestion',
        docs: {
            description: 'Enforce `includes` method over `indexOf` method',
            category: 'Best Practices',
            recommended: false,
            requiresTypeChecking: true,
        },
        fixable: 'code',
        messages: {
            preferIncludes: "Use 'includes()' method instead.",
            preferStringIncludes: 'Use `String#includes()` method with a string instead.',
        },
        schema: [],
    },
    create(context) {
        const globalScope = context.getScope();
        const services = util_1.getParserServices(context);
        const types = services.program.getTypeChecker();
        function isNumber(node, value) {
            const evaluated = util_1.getStaticValue(node, globalScope);
            return evaluated !== null && evaluated.value === value;
        }
        function isPositiveCheck(node) {
            switch (node.operator) {
                case '!==':
                case '!=':
                case '>':
                    return isNumber(node.right, -1);
                case '>=':
                    return isNumber(node.right, 0);
                default:
                    return false;
            }
        }
        function isNegativeCheck(node) {
            switch (node.operator) {
                case '===':
                case '==':
                case '<=':
                    return isNumber(node.right, -1);
                case '<':
                    return isNumber(node.right, 0);
                default:
                    return false;
            }
        }
        function hasSameParameters(nodeA, nodeB) {
            if (!ts.isFunctionLike(nodeA) || !ts.isFunctionLike(nodeB)) {
                return false;
            }
            const paramsA = nodeA.parameters;
            const paramsB = nodeB.parameters;
            if (paramsA.length !== paramsB.length) {
                return false;
            }
            for (let i = 0; i < paramsA.length; ++i) {
                const paramA = paramsA[i];
                const paramB = paramsB[i];
                // Check name, type, and question token once.
                if (paramA.getText() !== paramB.getText()) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Parse a given node if it's a `RegExp` instance.
         * @param node The node to parse.
         */
        function parseRegExp(node) {
            const evaluated = util_1.getStaticValue(node, globalScope);
            if (evaluated == null || !(evaluated.value instanceof RegExp)) {
                return null;
            }
            const { pattern, flags } = regexpp_1.parseRegExpLiteral(evaluated.value);
            if (pattern.alternatives.length !== 1 ||
                flags.ignoreCase ||
                flags.global) {
                return null;
            }
            // Check if it can determine a unique string.
            const chars = pattern.alternatives[0].elements;
            if (!chars.every(c => c.type === 'Character')) {
                return null;
            }
            // To string.
            return String.fromCodePoint(...chars.map(c => c.value));
        }
        return {
            [[
                // a.indexOf(b) !== 1
                "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]",
                // a?.indexOf(b) !== 1
                "BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]",
            ].join(', ')](node) {
                var _a, _b, _c;
                // Check if the comparison is equivalent to `includes()`.
                const callNode = node.parent;
                const compareNode = (((_a = callNode.parent) === null || _a === void 0 ? void 0 : _a.type) ===
                    experimental_utils_1.AST_NODE_TYPES.ChainExpression
                    ? callNode.parent.parent
                    : callNode.parent);
                const negative = isNegativeCheck(compareNode);
                if (!negative && !isPositiveCheck(compareNode)) {
                    return;
                }
                // Get the symbol of `indexOf` method.
                const tsNode = services.esTreeNodeToTSNodeMap.get(node.property);
                const indexofMethodDeclarations = (_b = types
                    .getSymbolAtLocation(tsNode)) === null || _b === void 0 ? void 0 : _b.getDeclarations();
                if (indexofMethodDeclarations == null ||
                    indexofMethodDeclarations.length === 0) {
                    return;
                }
                // Check if every declaration of `indexOf` method has `includes` method
                // and the two methods have the same parameters.
                for (const instanceofMethodDecl of indexofMethodDeclarations) {
                    const typeDecl = instanceofMethodDecl.parent;
                    const type = types.getTypeAtLocation(typeDecl);
                    const includesMethodDecl = (_c = type
                        .getProperty('includes')) === null || _c === void 0 ? void 0 : _c.getDeclarations();
                    if (includesMethodDecl == null ||
                        !includesMethodDecl.some(includesMethodDecl => hasSameParameters(includesMethodDecl, instanceofMethodDecl))) {
                        return;
                    }
                }
                // Report it.
                context.report({
                    node: compareNode,
                    messageId: 'preferIncludes',
                    *fix(fixer) {
                        if (negative) {
                            yield fixer.insertTextBefore(callNode, '!');
                        }
                        yield fixer.replaceText(node.property, 'includes');
                        yield fixer.removeRange([callNode.range[1], compareNode.range[1]]);
                    },
                });
            },
            // /bar/.test(foo)
            'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'(node) {
                var _a;
                const callNode = node.parent;
                const text = callNode.arguments.length === 1 ? parseRegExp(node.object) : null;
                if (text == null) {
                    return;
                }
                //check the argument type of test methods
                const argument = callNode.arguments[0];
                const tsNode = services.esTreeNodeToTSNodeMap.get(argument);
                const type = util_1.getConstrainedTypeAtLocation(types, tsNode);
                const includesMethodDecl = (_a = type
                    .getProperty('includes')) === null || _a === void 0 ? void 0 : _a.getDeclarations();
                if (includesMethodDecl == null) {
                    return;
                }
                context.report({
                    node: callNode,
                    messageId: 'preferStringIncludes',
                    *fix(fixer) {
                        const argNode = callNode.arguments[0];
                        const needsParen = argNode.type !== experimental_utils_1.AST_NODE_TYPES.Literal &&
                            argNode.type !== experimental_utils_1.AST_NODE_TYPES.TemplateLiteral &&
                            argNode.type !== experimental_utils_1.AST_NODE_TYPES.Identifier &&
                            argNode.type !== experimental_utils_1.AST_NODE_TYPES.MemberExpression &&
                            argNode.type !== experimental_utils_1.AST_NODE_TYPES.CallExpression;
                        yield fixer.removeRange([callNode.range[0], argNode.range[0]]);
                        if (needsParen) {
                            yield fixer.insertTextBefore(argNode, '(');
                            yield fixer.insertTextAfter(argNode, ')');
                        }
                        yield fixer.insertTextAfter(argNode, `${node.optional ? '?.' : '.'}includes(${JSON.stringify(text)}`);
                    },
                });
            },
        };
    },
});
//# sourceMappingURL=prefer-includes.js.map