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