UNPKG

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