UNPKG

9.35 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5}) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
7 o[k2] = m[k];
8}));
9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
11}) : function(o, v) {
12 o["default"] = v;
13});
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22const utils_1 = require("@typescript-eslint/utils");
23const regexpp_1 = require("regexpp");
24const ts = __importStar(require("typescript"));
25const util_1 = require("../util");
26exports.default = (0, util_1.createRule)({
27 name: 'prefer-includes',
28 defaultOptions: [],
29 meta: {
30 type: 'suggestion',
31 docs: {
32 description: 'Enforce `includes` method over `indexOf` method',
33 recommended: false,
34 requiresTypeChecking: true,
35 },
36 fixable: 'code',
37 messages: {
38 preferIncludes: "Use 'includes()' method instead.",
39 preferStringIncludes: 'Use `String#includes()` method with a string instead.',
40 },
41 schema: [],
42 },
43 create(context) {
44 const globalScope = context.getScope();
45 const services = (0, util_1.getParserServices)(context);
46 const types = services.program.getTypeChecker();
47 function isNumber(node, value) {
48 const evaluated = (0, util_1.getStaticValue)(node, globalScope);
49 return evaluated !== null && evaluated.value === value;
50 }
51 function isPositiveCheck(node) {
52 switch (node.operator) {
53 case '!==':
54 case '!=':
55 case '>':
56 return isNumber(node.right, -1);
57 case '>=':
58 return isNumber(node.right, 0);
59 default:
60 return false;
61 }
62 }
63 function isNegativeCheck(node) {
64 switch (node.operator) {
65 case '===':
66 case '==':
67 case '<=':
68 return isNumber(node.right, -1);
69 case '<':
70 return isNumber(node.right, 0);
71 default:
72 return false;
73 }
74 }
75 function hasSameParameters(nodeA, nodeB) {
76 if (!ts.isFunctionLike(nodeA) || !ts.isFunctionLike(nodeB)) {
77 return false;
78 }
79 const paramsA = nodeA.parameters;
80 const paramsB = nodeB.parameters;
81 if (paramsA.length !== paramsB.length) {
82 return false;
83 }
84 for (let i = 0; i < paramsA.length; ++i) {
85 const paramA = paramsA[i];
86 const paramB = paramsB[i];
87 // Check name, type, and question token once.
88 if (paramA.getText() !== paramB.getText()) {
89 return false;
90 }
91 }
92 return true;
93 }
94 /**
95 * Parse a given node if it's a `RegExp` instance.
96 * @param node The node to parse.
97 */
98 function parseRegExp(node) {
99 const evaluated = (0, util_1.getStaticValue)(node, globalScope);
100 if (evaluated == null || !(evaluated.value instanceof RegExp)) {
101 return null;
102 }
103 const { pattern, flags } = (0, regexpp_1.parseRegExpLiteral)(evaluated.value);
104 if (pattern.alternatives.length !== 1 ||
105 flags.ignoreCase ||
106 flags.global) {
107 return null;
108 }
109 // Check if it can determine a unique string.
110 const chars = pattern.alternatives[0].elements;
111 if (!chars.every(c => c.type === 'Character')) {
112 return null;
113 }
114 // To string.
115 return String.fromCodePoint(...chars.map(c => c.value));
116 }
117 function checkArrayIndexOf(node, allowFixing) {
118 var _a, _b, _c;
119 // Check if the comparison is equivalent to `includes()`.
120 const callNode = node.parent;
121 const compareNode = (((_a = callNode.parent) === null || _a === void 0 ? void 0 : _a.type) === utils_1.AST_NODE_TYPES.ChainExpression
122 ? callNode.parent.parent
123 : callNode.parent);
124 const negative = isNegativeCheck(compareNode);
125 if (!negative && !isPositiveCheck(compareNode)) {
126 return;
127 }
128 // Get the symbol of `indexOf` method.
129 const tsNode = services.esTreeNodeToTSNodeMap.get(node.property);
130 const indexofMethodDeclarations = (_b = types
131 .getSymbolAtLocation(tsNode)) === null || _b === void 0 ? void 0 : _b.getDeclarations();
132 if (indexofMethodDeclarations == null ||
133 indexofMethodDeclarations.length === 0) {
134 return;
135 }
136 // Check if every declaration of `indexOf` method has `includes` method
137 // and the two methods have the same parameters.
138 for (const instanceofMethodDecl of indexofMethodDeclarations) {
139 const typeDecl = instanceofMethodDecl.parent;
140 const type = types.getTypeAtLocation(typeDecl);
141 const includesMethodDecl = (_c = type
142 .getProperty('includes')) === null || _c === void 0 ? void 0 : _c.getDeclarations();
143 if (includesMethodDecl == null ||
144 !includesMethodDecl.some(includesMethodDecl => hasSameParameters(includesMethodDecl, instanceofMethodDecl))) {
145 return;
146 }
147 }
148 // Report it.
149 context.report(Object.assign({ node: compareNode, messageId: 'preferIncludes' }, (allowFixing && {
150 *fix(fixer) {
151 if (negative) {
152 yield fixer.insertTextBefore(callNode, '!');
153 }
154 yield fixer.replaceText(node.property, 'includes');
155 yield fixer.removeRange([callNode.range[1], compareNode.range[1]]);
156 },
157 })));
158 }
159 return {
160 // a.indexOf(b) !== 1
161 "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]"(node) {
162 checkArrayIndexOf(node, /* allowFixing */ true);
163 },
164 // a?.indexOf(b) !== 1
165 "BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]"(node) {
166 checkArrayIndexOf(node, /* allowFixing */ false);
167 },
168 // /bar/.test(foo)
169 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'(node) {
170 var _a;
171 const callNode = node.parent;
172 const text = callNode.arguments.length === 1 ? parseRegExp(node.object) : null;
173 if (text == null) {
174 return;
175 }
176 //check the argument type of test methods
177 const argument = callNode.arguments[0];
178 const tsNode = services.esTreeNodeToTSNodeMap.get(argument);
179 const type = (0, util_1.getConstrainedTypeAtLocation)(types, tsNode);
180 const includesMethodDecl = (_a = type
181 .getProperty('includes')) === null || _a === void 0 ? void 0 : _a.getDeclarations();
182 if (includesMethodDecl == null) {
183 return;
184 }
185 context.report({
186 node: callNode,
187 messageId: 'preferStringIncludes',
188 *fix(fixer) {
189 const argNode = callNode.arguments[0];
190 const needsParen = argNode.type !== utils_1.AST_NODE_TYPES.Literal &&
191 argNode.type !== utils_1.AST_NODE_TYPES.TemplateLiteral &&
192 argNode.type !== utils_1.AST_NODE_TYPES.Identifier &&
193 argNode.type !== utils_1.AST_NODE_TYPES.MemberExpression &&
194 argNode.type !== utils_1.AST_NODE_TYPES.CallExpression;
195 yield fixer.removeRange([callNode.range[0], argNode.range[0]]);
196 if (needsParen) {
197 yield fixer.insertTextBefore(argNode, '(');
198 yield fixer.insertTextAfter(argNode, ')');
199 }
200 yield fixer.insertTextAfter(argNode, `${node.optional ? '?.' : '.'}includes('${text}'`);
201 },
202 });
203 },
204 };
205 },
206});
207//# sourceMappingURL=prefer-includes.js.map
\No newline at end of file