UNPKG

8.34 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to disallow the type conversions with shorter notations.
3 * @author Toru Nagashima
4 * @copyright 2015 Toru Nagashima. All rights reserved.
5 */
6
7"use strict";
8
9//------------------------------------------------------------------------------
10// Helpers
11//------------------------------------------------------------------------------
12
13var INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/;
14var ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"];
15
16/**
17 * Parses and normalizes an option object.
18 * @param {object} options - An option object to parse.
19 * @returns {object} The parsed and normalized option object.
20 */
21function parseOptions(options) {
22 options = 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 var 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 or not a node is a concatenating with an empty string.
111 * @param {ASTNode} node - A BinaryExpression node to check.
112 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
113 */
114function isConcatWithEmptyString(node) {
115 return node.operator === "+" && (
116 (node.left.type === "Literal" && node.left.value === "") ||
117 (node.right.type === "Literal" && node.right.value === "")
118 );
119}
120
121/**
122 * Checks whether or not a node is appended with an empty string.
123 * @param {ASTNode} node - An AssignmentExpression node to check.
124 * @returns {boolean} Whether or not the node is appended with an empty string.
125 */
126function isAppendEmptyString(node) {
127 return node.operator === "+=" && node.right.type === "Literal" && node.right.value === "";
128}
129
130/**
131 * Gets a node that is the left or right operand of a node, is not the specified literal.
132 * @param {ASTNode} node - A BinaryExpression node to get.
133 * @param {any} value - A literal value to check.
134 * @returns {ASTNode} A node that is the left or right operand of the node, is not the specified literal.
135 */
136function getOtherOperand(node, value) {
137 if (node.left.type === "Literal" && node.left.value === value) {
138 return node.right;
139 }
140 return node.left;
141}
142//------------------------------------------------------------------------------
143// Rule Definition
144//------------------------------------------------------------------------------
145
146module.exports = function(context) {
147 var options = parseOptions(context.options[0]),
148 operatorAllowed = false;
149
150 return {
151 "UnaryExpression": function(node) {
152 // !!foo
153 operatorAllowed = options.allow.indexOf("!!") >= 0;
154 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
155 context.report(
156 node,
157 "use `Boolean({{code}})` instead.", {
158 code: context.getSource(node.argument.argument)
159 });
160 }
161 // ~foo.indexOf(bar)
162 operatorAllowed = options.allow.indexOf("~") >= 0;
163 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
164 context.report(
165 node,
166 "use `{{code}} !== -1` instead.", {
167 code: context.getSource(node.argument)
168 });
169 }
170
171 // +foo
172 operatorAllowed = options.allow.indexOf("+") >= 0;
173 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
174 context.report(
175 node,
176 "use `Number({{code}})` instead.", {
177 code: context.getSource(node.argument)
178 });
179 }
180 },
181
182 // Use `:exit` to prevent double reporting
183 "BinaryExpression:exit": function(node) {
184 // 1 * foo
185 operatorAllowed = options.allow.indexOf("*") >= 0;
186 var nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
187 if (nonNumericOperand) {
188 context.report(
189 node,
190 "use `Number({{code}})` instead.", {
191 code: context.getSource(nonNumericOperand)
192 });
193 }
194
195 // "" + foo
196 operatorAllowed = options.allow.indexOf("+") >= 0;
197 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
198 context.report(
199 node,
200 "use `String({{code}})` instead.", {
201 code: context.getSource(getOtherOperand(node, ""))
202 });
203 }
204 },
205
206 "AssignmentExpression": function(node) {
207 // foo += ""
208 operatorAllowed = options.allow.indexOf("+") >= 0;
209 if (options.string && isAppendEmptyString(node)) {
210 context.report(
211 node,
212 "use `{{code}} = String({{code}})` instead.", {
213 code: context.getSource(getOtherOperand(node, ""))
214 });
215 }
216 }
217 };
218};
219
220module.exports.schema = [{
221 "type": "object",
222 "properties": {
223 "boolean": {
224 "type": "boolean"
225 },
226 "number": {
227 "type": "boolean"
228 },
229 "string": {
230 "type": "boolean"
231 },
232 "allow": {
233 "type": "array",
234 "items": {
235 "enum": ALLOWABLE_OPERATORS
236 },
237 "uniqueItems": true
238 }
239 },
240 "additionalProperties": false
241}];