]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/no-implicit-coercion.js
f646093f55581b7450dca85a5d5f97d42bcaba8c
[pve-eslint.git] / 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 /** @type {import('../shared/types').Rule} */
171 module.exports = {
172 meta: {
173 type: "suggestion",
174
175 docs: {
176 description: "disallow shorthand type conversions",
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 };