source: imaps-frontend/node_modules/eslint/lib/rules/no-implicit-coercion.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 13.0 KB
Line 
1/**
2 * @fileoverview A rule to disallow the type conversions with shorter notations.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8const astUtils = require("./utils/ast-utils");
9
10//------------------------------------------------------------------------------
11// Helpers
12//------------------------------------------------------------------------------
13
14const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u;
15const 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 */
22function 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 negating.
34 * @param {ASTNode} node An UnaryExpression node to check.
35 * @returns {boolean} Whether or not the node is a double logical negating.
36 */
37function 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 */
50function 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 */
67function 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 given node logically represents multiplication by a fraction of `1`.
76 * For example, `a * 1` in `a * 1 / b` is technically multiplication by `1`, but the
77 * whole expression can be logically interpreted as `a * (1 / b)` rather than `(a * 1) / b`.
78 * @param {BinaryExpression} node A BinaryExpression node to check.
79 * @param {SourceCode} sourceCode The source code object.
80 * @returns {boolean} Whether or not the node is a multiplying by a fraction of `1`.
81 */
82function isMultiplyByFractionOfOne(node, sourceCode) {
83 return node.type === "BinaryExpression" &&
84 node.operator === "*" &&
85 (node.right.type === "Literal" && node.right.value === 1) &&
86 node.parent.type === "BinaryExpression" &&
87 node.parent.operator === "/" &&
88 node.parent.left === node &&
89 !astUtils.isParenthesised(sourceCode, node);
90}
91
92/**
93 * Checks whether the result of a node is numeric or not
94 * @param {ASTNode} node The node to test
95 * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
96 */
97function isNumeric(node) {
98 return (
99 node.type === "Literal" && typeof node.value === "number" ||
100 node.type === "CallExpression" && (
101 node.callee.name === "Number" ||
102 node.callee.name === "parseInt" ||
103 node.callee.name === "parseFloat"
104 )
105 );
106}
107
108/**
109 * Returns the first non-numeric operand in a BinaryExpression. Designed to be
110 * used from bottom to up since it walks up the BinaryExpression trees using
111 * node.parent to find the result.
112 * @param {BinaryExpression} node The BinaryExpression node to be walked up on
113 * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
114 */
115function getNonNumericOperand(node) {
116 const left = node.left,
117 right = node.right;
118
119 if (right.type !== "BinaryExpression" && !isNumeric(right)) {
120 return right;
121 }
122
123 if (left.type !== "BinaryExpression" && !isNumeric(left)) {
124 return left;
125 }
126
127 return null;
128}
129
130/**
131 * Checks whether an expression evaluates to a string.
132 * @param {ASTNode} node node that represents the expression to check.
133 * @returns {boolean} Whether or not the expression evaluates to a string.
134 */
135function isStringType(node) {
136 return astUtils.isStringLiteral(node) ||
137 (
138 node.type === "CallExpression" &&
139 node.callee.type === "Identifier" &&
140 node.callee.name === "String"
141 );
142}
143
144/**
145 * Checks whether a node is an empty string literal or not.
146 * @param {ASTNode} node The node to check.
147 * @returns {boolean} Whether or not the passed in node is an
148 * empty string literal or not.
149 */
150function isEmptyString(node) {
151 return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
152}
153
154/**
155 * Checks whether or not a node is a concatenating with an empty string.
156 * @param {ASTNode} node A BinaryExpression node to check.
157 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
158 */
159function isConcatWithEmptyString(node) {
160 return node.operator === "+" && (
161 (isEmptyString(node.left) && !isStringType(node.right)) ||
162 (isEmptyString(node.right) && !isStringType(node.left))
163 );
164}
165
166/**
167 * Checks whether or not a node is appended with an empty string.
168 * @param {ASTNode} node An AssignmentExpression node to check.
169 * @returns {boolean} Whether or not the node is appended with an empty string.
170 */
171function isAppendEmptyString(node) {
172 return node.operator === "+=" && isEmptyString(node.right);
173}
174
175/**
176 * Returns the operand that is not an empty string from a flagged BinaryExpression.
177 * @param {ASTNode} node The flagged BinaryExpression node to check.
178 * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
179 */
180function getNonEmptyOperand(node) {
181 return isEmptyString(node.left) ? node.right : node.left;
182}
183
184//------------------------------------------------------------------------------
185// Rule Definition
186//------------------------------------------------------------------------------
187
188/** @type {import('../shared/types').Rule} */
189module.exports = {
190 meta: {
191 type: "suggestion",
192
193 docs: {
194 description: "Disallow shorthand type conversions",
195 recommended: false,
196 url: "https://eslint.org/docs/latest/rules/no-implicit-coercion"
197 },
198
199 fixable: "code",
200
201 schema: [{
202 type: "object",
203 properties: {
204 boolean: {
205 type: "boolean",
206 default: true
207 },
208 number: {
209 type: "boolean",
210 default: true
211 },
212 string: {
213 type: "boolean",
214 default: true
215 },
216 disallowTemplateShorthand: {
217 type: "boolean",
218 default: false
219 },
220 allow: {
221 type: "array",
222 items: {
223 enum: ALLOWABLE_OPERATORS
224 },
225 uniqueItems: true
226 }
227 },
228 additionalProperties: false
229 }],
230
231 messages: {
232 useRecommendation: "use `{{recommendation}}` instead."
233 }
234 },
235
236 create(context) {
237 const options = parseOptions(context.options[0] || {});
238 const sourceCode = context.sourceCode;
239
240 /**
241 * Reports an error and autofixes the node
242 * @param {ASTNode} node An ast node to report the error on.
243 * @param {string} recommendation The recommended code for the issue
244 * @param {bool} shouldFix Whether this report should fix the node
245 * @returns {void}
246 */
247 function report(node, recommendation, shouldFix) {
248 context.report({
249 node,
250 messageId: "useRecommendation",
251 data: {
252 recommendation
253 },
254 fix(fixer) {
255 if (!shouldFix) {
256 return null;
257 }
258
259 const tokenBefore = sourceCode.getTokenBefore(node);
260
261 if (
262 tokenBefore &&
263 tokenBefore.range[1] === node.range[0] &&
264 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
265 ) {
266 return fixer.replaceText(node, ` ${recommendation}`);
267 }
268 return fixer.replaceText(node, recommendation);
269 }
270 });
271 }
272
273 return {
274 UnaryExpression(node) {
275 let operatorAllowed;
276
277 // !!foo
278 operatorAllowed = options.allow.includes("!!");
279 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
280 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
281
282 report(node, recommendation, true);
283 }
284
285 // ~foo.indexOf(bar)
286 operatorAllowed = options.allow.includes("~");
287 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
288
289 // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
290 const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
291 const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
292
293 report(node, recommendation, false);
294 }
295
296 // +foo
297 operatorAllowed = options.allow.includes("+");
298 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
299 const recommendation = `Number(${sourceCode.getText(node.argument)})`;
300
301 report(node, recommendation, true);
302 }
303 },
304
305 // Use `:exit` to prevent double reporting
306 "BinaryExpression:exit"(node) {
307 let operatorAllowed;
308
309 // 1 * foo
310 operatorAllowed = options.allow.includes("*");
311 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && !isMultiplyByFractionOfOne(node, sourceCode) &&
312 getNonNumericOperand(node);
313
314 if (nonNumericOperand) {
315 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
316
317 report(node, recommendation, true);
318 }
319
320 // "" + foo
321 operatorAllowed = options.allow.includes("+");
322 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
323 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
324
325 report(node, recommendation, true);
326 }
327 },
328
329 AssignmentExpression(node) {
330
331 // foo += ""
332 const operatorAllowed = options.allow.includes("+");
333
334 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
335 const code = sourceCode.getText(getNonEmptyOperand(node));
336 const recommendation = `${code} = String(${code})`;
337
338 report(node, recommendation, true);
339 }
340 },
341
342 TemplateLiteral(node) {
343 if (!options.disallowTemplateShorthand) {
344 return;
345 }
346
347 // tag`${foo}`
348 if (node.parent.type === "TaggedTemplateExpression") {
349 return;
350 }
351
352 // `` or `${foo}${bar}`
353 if (node.expressions.length !== 1) {
354 return;
355 }
356
357
358 // `prefix${foo}`
359 if (node.quasis[0].value.cooked !== "") {
360 return;
361 }
362
363 // `${foo}postfix`
364 if (node.quasis[1].value.cooked !== "") {
365 return;
366 }
367
368 // if the expression is already a string, then this isn't a coercion
369 if (isStringType(node.expressions[0])) {
370 return;
371 }
372
373 const code = sourceCode.getText(node.expressions[0]);
374 const recommendation = `String(${code})`;
375
376 report(node, recommendation, true);
377 }
378 };
379 }
380};
Note: See TracBrowser for help on using the repository browser.