UNPKG

11.7 kBJavaScriptView Raw
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 nigating.
34 * @param {ASTNode} node An UnaryExpression node to check.
35 * @returns {boolean} Whether or not the node is a double logical nigating.
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 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 */
79function 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 */
97function 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 a node is an empty string literal or not.
114 * @param {ASTNode} node The node to check.
115 * @returns {boolean} Whether or not the passed in node is an
116 * empty string literal or not.
117 */
118function isEmptyString(node) {
119 return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
120}
121
122/**
123 * Checks whether or not a node is a concatenating with an empty string.
124 * @param {ASTNode} node A BinaryExpression node to check.
125 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
126 */
127function isConcatWithEmptyString(node) {
128 return node.operator === "+" && (
129 (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
130 (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
131 );
132}
133
134/**
135 * Checks whether or not a node is appended with an empty string.
136 * @param {ASTNode} node An AssignmentExpression node to check.
137 * @returns {boolean} Whether or not the node is appended with an empty string.
138 */
139function isAppendEmptyString(node) {
140 return node.operator === "+=" && isEmptyString(node.right);
141}
142
143/**
144 * Returns the operand that is not an empty string from a flagged BinaryExpression.
145 * @param {ASTNode} node The flagged BinaryExpression node to check.
146 * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
147 */
148function getNonEmptyOperand(node) {
149 return isEmptyString(node.left) ? node.right : node.left;
150}
151
152//------------------------------------------------------------------------------
153// Rule Definition
154//------------------------------------------------------------------------------
155
156module.exports = {
157 meta: {
158 type: "suggestion",
159
160 docs: {
161 description: "disallow shorthand type conversions",
162 category: "Best Practices",
163 recommended: false,
164 url: "https://eslint.org/docs/rules/no-implicit-coercion"
165 },
166
167 fixable: "code",
168
169 schema: [{
170 type: "object",
171 properties: {
172 boolean: {
173 type: "boolean",
174 default: true
175 },
176 number: {
177 type: "boolean",
178 default: true
179 },
180 string: {
181 type: "boolean",
182 default: true
183 },
184 disallowTemplateShorthand: {
185 type: "boolean",
186 default: false
187 },
188 allow: {
189 type: "array",
190 items: {
191 enum: ALLOWABLE_OPERATORS
192 },
193 uniqueItems: true
194 }
195 },
196 additionalProperties: false
197 }],
198
199 messages: {
200 useRecommendation: "use `{{recommendation}}` instead."
201 }
202 },
203
204 create(context) {
205 const options = parseOptions(context.options[0] || {});
206 const sourceCode = context.getSourceCode();
207
208 /**
209 * Reports an error and autofixes the node
210 * @param {ASTNode} node An ast node to report the error on.
211 * @param {string} recommendation The recommended code for the issue
212 * @param {bool} shouldFix Whether this report should fix the node
213 * @returns {void}
214 */
215 function report(node, recommendation, shouldFix) {
216 context.report({
217 node,
218 messageId: "useRecommendation",
219 data: {
220 recommendation
221 },
222 fix(fixer) {
223 if (!shouldFix) {
224 return null;
225 }
226
227 const tokenBefore = sourceCode.getTokenBefore(node);
228
229 if (
230 tokenBefore &&
231 tokenBefore.range[1] === node.range[0] &&
232 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
233 ) {
234 return fixer.replaceText(node, ` ${recommendation}`);
235 }
236 return fixer.replaceText(node, recommendation);
237 }
238 });
239 }
240
241 return {
242 UnaryExpression(node) {
243 let operatorAllowed;
244
245 // !!foo
246 operatorAllowed = options.allow.indexOf("!!") >= 0;
247 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
248 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
249
250 report(node, recommendation, true);
251 }
252
253 // ~foo.indexOf(bar)
254 operatorAllowed = options.allow.indexOf("~") >= 0;
255 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
256
257 // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
258 const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
259 const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
260
261 report(node, recommendation, false);
262 }
263
264 // +foo
265 operatorAllowed = options.allow.indexOf("+") >= 0;
266 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
267 const recommendation = `Number(${sourceCode.getText(node.argument)})`;
268
269 report(node, recommendation, true);
270 }
271 },
272
273 // Use `:exit` to prevent double reporting
274 "BinaryExpression:exit"(node) {
275 let operatorAllowed;
276
277 // 1 * foo
278 operatorAllowed = options.allow.indexOf("*") >= 0;
279 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
280
281 if (nonNumericOperand) {
282 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
283
284 report(node, recommendation, true);
285 }
286
287 // "" + foo
288 operatorAllowed = options.allow.indexOf("+") >= 0;
289 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
290 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
291
292 report(node, recommendation, true);
293 }
294 },
295
296 AssignmentExpression(node) {
297
298 // foo += ""
299 const operatorAllowed = options.allow.indexOf("+") >= 0;
300
301 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
302 const code = sourceCode.getText(getNonEmptyOperand(node));
303 const recommendation = `${code} = String(${code})`;
304
305 report(node, recommendation, true);
306 }
307 },
308
309 TemplateLiteral(node) {
310 if (!options.disallowTemplateShorthand) {
311 return;
312 }
313
314 // tag`${foo}`
315 if (node.parent.type === "TaggedTemplateExpression") {
316 return;
317 }
318
319 // `` or `${foo}${bar}`
320 if (node.expressions.length !== 1) {
321 return;
322 }
323
324
325 // `prefix${foo}`
326 if (node.quasis[0].value.cooked !== "") {
327 return;
328 }
329
330 // `${foo}postfix`
331 if (node.quasis[1].value.cooked !== "") {
332 return;
333 }
334
335 const code = sourceCode.getText(node.expressions[0]);
336 const recommendation = `String(${code})`;
337
338 report(node, recommendation, true);
339 }
340 };
341 }
342};