.gitignore added
[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         allow: options.allow || []
28     };
29 }
30
31 /**
32  * Checks whether or not a node is a double logical nigating.
33  * @param {ASTNode} node An UnaryExpression node to check.
34  * @returns {boolean} Whether or not the node is a double logical nigating.
35  */
36 function isDoubleLogicalNegating(node) {
37     return (
38         node.operator === "!" &&
39         node.argument.type === "UnaryExpression" &&
40         node.argument.operator === "!"
41     );
42 }
43
44 /**
45  * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
46  * @param {ASTNode} node An UnaryExpression node to check.
47  * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
48  */
49 function isBinaryNegatingOfIndexOf(node) {
50     if (node.operator !== "~") {
51         return false;
52     }
53     const callNode = astUtils.skipChainExpression(node.argument);
54
55     return (
56         callNode.type === "CallExpression" &&
57         astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
58     );
59 }
60
61 /**
62  * Checks whether or not a node is a multiplying by one.
63  * @param {BinaryExpression} node A BinaryExpression node to check.
64  * @returns {boolean} Whether or not the node is a multiplying by one.
65  */
66 function isMultiplyByOne(node) {
67     return node.operator === "*" && (
68         node.left.type === "Literal" && node.left.value === 1 ||
69         node.right.type === "Literal" && node.right.value === 1
70     );
71 }
72
73 /**
74  * Checks whether the result of a node is numeric or not
75  * @param {ASTNode} node The node to test
76  * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
77  */
78 function isNumeric(node) {
79     return (
80         node.type === "Literal" && typeof node.value === "number" ||
81         node.type === "CallExpression" && (
82             node.callee.name === "Number" ||
83             node.callee.name === "parseInt" ||
84             node.callee.name === "parseFloat"
85         )
86     );
87 }
88
89 /**
90  * Returns the first non-numeric operand in a BinaryExpression. Designed to be
91  * used from bottom to up since it walks up the BinaryExpression trees using
92  * node.parent to find the result.
93  * @param {BinaryExpression} node The BinaryExpression node to be walked up on
94  * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
95  */
96 function getNonNumericOperand(node) {
97     const left = node.left,
98         right = node.right;
99
100     if (right.type !== "BinaryExpression" && !isNumeric(right)) {
101         return right;
102     }
103
104     if (left.type !== "BinaryExpression" && !isNumeric(left)) {
105         return left;
106     }
107
108     return null;
109 }
110
111 /**
112  * Checks whether a node is an empty string literal or not.
113  * @param {ASTNode} node The node to check.
114  * @returns {boolean} Whether or not the passed in node is an
115  * empty string literal or not.
116  */
117 function isEmptyString(node) {
118     return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
119 }
120
121 /**
122  * Checks whether or not a node is a concatenating with an empty string.
123  * @param {ASTNode} node A BinaryExpression node to check.
124  * @returns {boolean} Whether or not the node is a concatenating with an empty string.
125  */
126 function isConcatWithEmptyString(node) {
127     return node.operator === "+" && (
128         (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
129         (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
130     );
131 }
132
133 /**
134  * Checks whether or not a node is appended with an empty string.
135  * @param {ASTNode} node An AssignmentExpression node to check.
136  * @returns {boolean} Whether or not the node is appended with an empty string.
137  */
138 function isAppendEmptyString(node) {
139     return node.operator === "+=" && isEmptyString(node.right);
140 }
141
142 /**
143  * Returns the operand that is not an empty string from a flagged BinaryExpression.
144  * @param {ASTNode} node The flagged BinaryExpression node to check.
145  * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
146  */
147 function getNonEmptyOperand(node) {
148     return isEmptyString(node.left) ? node.right : node.left;
149 }
150
151 //------------------------------------------------------------------------------
152 // Rule Definition
153 //------------------------------------------------------------------------------
154
155 module.exports = {
156     meta: {
157         type: "suggestion",
158
159         docs: {
160             description: "disallow shorthand type conversions",
161             category: "Best Practices",
162             recommended: false,
163             url: "https://eslint.org/docs/rules/no-implicit-coercion"
164         },
165
166         fixable: "code",
167
168         schema: [{
169             type: "object",
170             properties: {
171                 boolean: {
172                     type: "boolean",
173                     default: true
174                 },
175                 number: {
176                     type: "boolean",
177                     default: true
178                 },
179                 string: {
180                     type: "boolean",
181                     default: true
182                 },
183                 allow: {
184                     type: "array",
185                     items: {
186                         enum: ALLOWABLE_OPERATORS
187                     },
188                     uniqueItems: true
189                 }
190             },
191             additionalProperties: false
192         }],
193
194         messages: {
195             useRecommendation: "use `{{recommendation}}` instead."
196         }
197     },
198
199     create(context) {
200         const options = parseOptions(context.options[0] || {});
201         const sourceCode = context.getSourceCode();
202
203         /**
204          * Reports an error and autofixes the node
205          * @param {ASTNode} node An ast node to report the error on.
206          * @param {string} recommendation The recommended code for the issue
207          * @param {bool} shouldFix Whether this report should fix the node
208          * @returns {void}
209          */
210         function report(node, recommendation, shouldFix) {
211             context.report({
212                 node,
213                 messageId: "useRecommendation",
214                 data: {
215                     recommendation
216                 },
217                 fix(fixer) {
218                     if (!shouldFix) {
219                         return null;
220                     }
221
222                     const tokenBefore = sourceCode.getTokenBefore(node);
223
224                     if (
225                         tokenBefore &&
226                         tokenBefore.range[1] === node.range[0] &&
227                         !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
228                     ) {
229                         return fixer.replaceText(node, ` ${recommendation}`);
230                     }
231                     return fixer.replaceText(node, recommendation);
232                 }
233             });
234         }
235
236         return {
237             UnaryExpression(node) {
238                 let operatorAllowed;
239
240                 // !!foo
241                 operatorAllowed = options.allow.indexOf("!!") >= 0;
242                 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
243                     const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
244
245                     report(node, recommendation, true);
246                 }
247
248                 // ~foo.indexOf(bar)
249                 operatorAllowed = options.allow.indexOf("~") >= 0;
250                 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
251
252                     // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
253                     const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
254                     const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
255
256                     report(node, recommendation, false);
257                 }
258
259                 // +foo
260                 operatorAllowed = options.allow.indexOf("+") >= 0;
261                 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
262                     const recommendation = `Number(${sourceCode.getText(node.argument)})`;
263
264                     report(node, recommendation, true);
265                 }
266             },
267
268             // Use `:exit` to prevent double reporting
269             "BinaryExpression:exit"(node) {
270                 let operatorAllowed;
271
272                 // 1 * foo
273                 operatorAllowed = options.allow.indexOf("*") >= 0;
274                 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
275
276                 if (nonNumericOperand) {
277                     const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
278
279                     report(node, recommendation, true);
280                 }
281
282                 // "" + foo
283                 operatorAllowed = options.allow.indexOf("+") >= 0;
284                 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
285                     const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
286
287                     report(node, recommendation, true);
288                 }
289             },
290
291             AssignmentExpression(node) {
292
293                 // foo += ""
294                 const operatorAllowed = options.allow.indexOf("+") >= 0;
295
296                 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
297                     const code = sourceCode.getText(getNonEmptyOperand(node));
298                     const recommendation = `${code} = String(${code})`;
299
300                     report(node, recommendation, true);
301                 }
302             }
303         };
304     }
305 };