UNPKG

10.2 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("../ast-utils");
9
10//------------------------------------------------------------------------------
11// Helpers
12//------------------------------------------------------------------------------
13
14const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/;
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 ? Boolean(options.boolean) : true,
25 number: "number" in options ? Boolean(options.number) : true,
26 string: "string" in options ? Boolean(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 docs: {
156 description: "disallow shorthand type conversions",
157 category: "Best Practices",
158 recommended: false,
159 url: "https://eslint.org/docs/rules/no-implicit-coercion"
160 },
161
162 fixable: "code",
163 schema: [{
164 type: "object",
165 properties: {
166 boolean: {
167 type: "boolean"
168 },
169 number: {
170 type: "boolean"
171 },
172 string: {
173 type: "boolean"
174 },
175 allow: {
176 type: "array",
177 items: {
178 enum: ALLOWABLE_OPERATORS
179 },
180 uniqueItems: true
181 }
182 },
183 additionalProperties: false
184 }]
185 },
186
187 create(context) {
188 const options = parseOptions(context.options[0] || {});
189 const sourceCode = context.getSourceCode();
190
191 /**
192 * Reports an error and autofixes the node
193 * @param {ASTNode} node - An ast node to report the error on.
194 * @param {string} recommendation - The recommended code for the issue
195 * @param {bool} shouldFix - Whether this report should fix the node
196 * @returns {void}
197 */
198 function report(node, recommendation, shouldFix) {
199 context.report({
200 node,
201 message: "use `{{recommendation}}` instead.",
202 data: {
203 recommendation
204 },
205 fix(fixer) {
206 if (!shouldFix) {
207 return null;
208 }
209
210 const tokenBefore = sourceCode.getTokenBefore(node);
211
212 if (
213 tokenBefore &&
214 tokenBefore.range[1] === node.range[0] &&
215 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
216 ) {
217 return fixer.replaceText(node, ` ${recommendation}`);
218 }
219 return fixer.replaceText(node, recommendation);
220 }
221 });
222 }
223
224 return {
225 UnaryExpression(node) {
226 let operatorAllowed;
227
228 // !!foo
229 operatorAllowed = options.allow.indexOf("!!") >= 0;
230 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
231 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
232
233 report(node, recommendation, true);
234 }
235
236 // ~foo.indexOf(bar)
237 operatorAllowed = options.allow.indexOf("~") >= 0;
238 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
239 const recommendation = `${sourceCode.getText(node.argument)} !== -1`;
240
241 report(node, recommendation, false);
242 }
243
244 // +foo
245 operatorAllowed = options.allow.indexOf("+") >= 0;
246 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
247 const recommendation = `Number(${sourceCode.getText(node.argument)})`;
248
249 report(node, recommendation, true);
250 }
251 },
252
253 // Use `:exit` to prevent double reporting
254 "BinaryExpression:exit"(node) {
255 let operatorAllowed;
256
257 // 1 * foo
258 operatorAllowed = options.allow.indexOf("*") >= 0;
259 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
260
261 if (nonNumericOperand) {
262 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
263
264 report(node, recommendation, true);
265 }
266
267 // "" + foo
268 operatorAllowed = options.allow.indexOf("+") >= 0;
269 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
270 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
271
272 report(node, recommendation, true);
273 }
274 },
275
276 AssignmentExpression(node) {
277
278 // foo += ""
279 const operatorAllowed = options.allow.indexOf("+") >= 0;
280
281 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
282 const code = sourceCode.getText(getNonEmptyOperand(node));
283 const recommendation = `${code} = String(${code})`;
284
285 report(node, recommendation, true);
286 }
287 }
288 };
289 }
290};