massive update, probably broken
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / no-implicit-coercion.js
1 /**
2  * @fileoverview A rule to disallow the type conversions with shorter notations.
3  * @author Toru Nagashima
4  */
5
6 "use strict";
7
8 const astUtils = require("./utils/ast-utils");
9
10 //------------------------------------------------------------------------------
11 // Helpers
12 //------------------------------------------------------------------------------
13
14 const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u;
15 const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"];
16
17 /**
18  * Parses and normalizes an option object.
19  * @param {Object} options An option object to parse.
20  * @returns {Object} The parsed and normalized option object.
21  */
22 function parseOptions(options) {
23     return {
24         boolean: "boolean" in options ? options.boolean : true,
25         number: "number" in options ? options.number : true,
26         string: "string" in options ? options.string : true,
27         disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false,
28         allow: options.allow || []
29     };
30 }
31
32 /**
33  * Checks whether or not a node is a double logical nigating.
34  * @param {ASTNode} node An UnaryExpression node to check.
35  * @returns {boolean} Whether or not the node is a double logical nigating.
36  */
37 function isDoubleLogicalNegating(node) {
38     return (
39         node.operator === "!" &&
40         node.argument.type === "UnaryExpression" &&
41         node.argument.operator === "!"
42     );
43 }
44
45 /**
46  * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
47  * @param {ASTNode} node An UnaryExpression node to check.
48  * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
49  */
50 function isBinaryNegatingOfIndexOf(node) {
51     if (node.operator !== "~") {
52         return false;
53     }
54     const callNode = astUtils.skipChainExpression(node.argument);
55
56     return (
57         callNode.type === "CallExpression" &&
58         astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
59     );
60 }
61
62 /**
63  * Checks whether or not a node is a multiplying by one.
64  * @param {BinaryExpression} node A BinaryExpression node to check.
65  * @returns {boolean} Whether or not the node is a multiplying by one.
66  */
67 function isMultiplyByOne(node) {
68     return node.operator === "*" && (
69         node.left.type === "Literal" && node.left.value === 1 ||
70         node.right.type === "Literal" && node.right.value === 1
71     );
72 }
73
74 /**
75  * Checks whether the result of a node is numeric or not
76  * @param {ASTNode} node The node to test
77  * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
78  */
79 function isNumeric(node) {
80     return (
81         node.type === "Literal" && typeof node.value === "number" ||
82         node.type === "CallExpression" && (
83             node.callee.name === "Number" ||
84             node.callee.name === "parseInt" ||
85             node.callee.name === "parseFloat"
86         )
87     );
88 }
89
90 /**
91  * Returns the first non-numeric operand in a BinaryExpression. Designed to be
92  * used from bottom to up since it walks up the BinaryExpression trees using
93  * node.parent to find the result.
94  * @param {BinaryExpression} node The BinaryExpression node to be walked up on
95  * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
96  */
97 function getNonNumericOperand(node) {
98     const left = node.left,
99         right = node.right;
100
101     if (right.type !== "BinaryExpression" && !isNumeric(right)) {
102         return right;
103     }
104
105     if (left.type !== "BinaryExpression" && !isNumeric(left)) {
106         return left;
107     }
108
109     return null;
110 }
111
112 /**
113  * Checks whether an expression evaluates to a string.
114  * @param {ASTNode} node node that represents the expression to check.
115  * @returns {boolean} Whether or not the expression evaluates to a string.
116  */
117 function isStringType(node) {
118     return astUtils.isStringLiteral(node) ||
119         (
120             node.type === "CallExpression" &&
121             node.callee.type === "Identifier" &&
122             node.callee.name === "String"
123         );
124 }
125
126 /**
127  * Checks whether a node is an empty string literal or not.
128  * @param {ASTNode} node The node to check.
129  * @returns {boolean} Whether or not the passed in node is an
130  * empty string literal or not.
131  */
132 function isEmptyString(node) {
133     return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
134 }
135
136 /**
137  * Checks whether or not a node is a concatenating with an empty string.
138  * @param {ASTNode} node A BinaryExpression node to check.
139  * @returns {boolean} Whether or not the node is a concatenating with an empty string.
140  */
141 function isConcatWithEmptyString(node) {
142     return node.operator === "+" && (
143         (isEmptyString(node.left) && !isStringType(node.right)) ||
144         (isEmptyString(node.right) && !isStringType(node.left))
145     );
146 }
147
148 /**
149  * Checks whether or not a node is appended with an empty string.
150  * @param {ASTNode} node An AssignmentExpression node to check.
151  * @returns {boolean} Whether or not the node is appended with an empty string.
152  */
153 function isAppendEmptyString(node) {
154     return node.operator === "+=" && isEmptyString(node.right);
155 }
156
157 /**
158  * Returns the operand that is not an empty string from a flagged BinaryExpression.
159  * @param {ASTNode} node The flagged BinaryExpression node to check.
160  * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
161  */
162 function getNonEmptyOperand(node) {
163     return isEmptyString(node.left) ? node.right : node.left;
164 }
165
166 //------------------------------------------------------------------------------
167 // Rule Definition
168 //------------------------------------------------------------------------------
169
170 module.exports = {
171     meta: {
172         type: "suggestion",
173
174         docs: {
175             description: "disallow shorthand type conversions",
176             category: "Best Practices",
177             recommended: false,
178             url: "https://eslint.org/docs/rules/no-implicit-coercion"
179         },
180
181         fixable: "code",
182
183         schema: [{
184             type: "object",
185             properties: {
186                 boolean: {
187                     type: "boolean",
188                     default: true
189                 },
190                 number: {
191                     type: "boolean",
192                     default: true
193                 },
194                 string: {
195                     type: "boolean",
196                     default: true
197                 },
198                 disallowTemplateShorthand: {
199                     type: "boolean",
200                     default: false
201                 },
202                 allow: {
203                     type: "array",
204                     items: {
205                         enum: ALLOWABLE_OPERATORS
206                     },
207                     uniqueItems: true
208                 }
209             },
210             additionalProperties: false
211         }],
212
213         messages: {
214             useRecommendation: "use `{{recommendation}}` instead."
215         }
216     },
217
218     create(context) {
219         const options = parseOptions(context.options[0] || {});
220         const sourceCode = context.getSourceCode();
221
222         /**
223          * Reports an error and autofixes the node
224          * @param {ASTNode} node An ast node to report the error on.
225          * @param {string} recommendation The recommended code for the issue
226          * @param {bool} shouldFix Whether this report should fix the node
227          * @returns {void}
228          */
229         function report(node, recommendation, shouldFix) {
230             context.report({
231                 node,
232                 messageId: "useRecommendation",
233                 data: {
234                     recommendation
235                 },
236                 fix(fixer) {
237                     if (!shouldFix) {
238                         return null;
239                     }
240
241                     const tokenBefore = sourceCode.getTokenBefore(node);
242
243                     if (
244                         tokenBefore &&
245                         tokenBefore.range[1] === node.range[0] &&
246                         !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
247                     ) {
248                         return fixer.replaceText(node, ` ${recommendation}`);
249                     }
250                     return fixer.replaceText(node, recommendation);
251                 }
252             });
253         }
254
255         return {
256             UnaryExpression(node) {
257                 let operatorAllowed;
258
259                 // !!foo
260                 operatorAllowed = options.allow.indexOf("!!") >= 0;
261                 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
262                     const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
263
264                     report(node, recommendation, true);
265                 }
266
267                 // ~foo.indexOf(bar)
268                 operatorAllowed = options.allow.indexOf("~") >= 0;
269                 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
270
271                     // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
272                     const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
273                     const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
274
275                     report(node, recommendation, false);
276                 }
277
278                 // +foo
279                 operatorAllowed = options.allow.indexOf("+") >= 0;
280                 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
281                     const recommendation = `Number(${sourceCode.getText(node.argument)})`;
282
283                     report(node, recommendation, true);
284                 }
285             },
286
287             // Use `:exit` to prevent double reporting
288             "BinaryExpression:exit"(node) {
289                 let operatorAllowed;
290
291                 // 1 * foo
292                 operatorAllowed = options.allow.indexOf("*") >= 0;
293                 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
294
295                 if (nonNumericOperand) {
296                     const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
297
298                     report(node, recommendation, true);
299                 }
300
301                 // "" + foo
302                 operatorAllowed = options.allow.indexOf("+") >= 0;
303                 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
304                     const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
305
306                     report(node, recommendation, true);
307                 }
308             },
309
310             AssignmentExpression(node) {
311
312                 // foo += ""
313                 const operatorAllowed = options.allow.indexOf("+") >= 0;
314
315                 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
316                     const code = sourceCode.getText(getNonEmptyOperand(node));
317                     const recommendation = `${code} = String(${code})`;
318
319                     report(node, recommendation, true);
320                 }
321             },
322
323             TemplateLiteral(node) {
324                 if (!options.disallowTemplateShorthand) {
325                     return;
326                 }
327
328                 // tag`${foo}`
329                 if (node.parent.type === "TaggedTemplateExpression") {
330                     return;
331                 }
332
333                 // `` or `${foo}${bar}`
334                 if (node.expressions.length !== 1) {
335                     return;
336                 }
337
338
339                 //  `prefix${foo}`
340                 if (node.quasis[0].value.cooked !== "") {
341                     return;
342                 }
343
344                 //  `${foo}postfix`
345                 if (node.quasis[1].value.cooked !== "") {
346                     return;
347                 }
348
349                 // if the expression is already a string, then this isn't a coercion
350                 if (isStringType(node.expressions[0])) {
351                     return;
352                 }
353
354                 const code = sourceCode.getText(node.expressions[0]);
355                 const recommendation = `String(${code})`;
356
357                 report(node, recommendation, true);
358             }
359         };
360     }
361 };