UNPKG

9.41 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to require or disallow yoda comparisons
3 * @author Nicholas C. Zakas
4 */
5"use strict";
6
7//--------------------------------------------------------------------------
8// Helpers
9//--------------------------------------------------------------------------
10
11/**
12 * Determines whether an operator is a comparison operator.
13 * @param {string} operator The operator to check.
14 * @returns {boolean} Whether or not it is a comparison operator.
15 */
16function isComparisonOperator(operator) {
17 return (/^(==|===|!=|!==|<|>|<=|>=)$/).test(operator);
18}
19
20/**
21 * Determines whether an operator is an equality operator.
22 * @param {string} operator The operator to check.
23 * @returns {boolean} Whether or not it is an equality operator.
24 */
25function isEqualityOperator(operator) {
26 return (/^(==|===)$/).test(operator);
27}
28
29/**
30 * Determines whether an operator is one used in a range test.
31 * Allowed operators are `<` and `<=`.
32 * @param {string} operator The operator to check.
33 * @returns {boolean} Whether the operator is used in range tests.
34 */
35function isRangeTestOperator(operator) {
36 return ["<", "<="].indexOf(operator) >= 0;
37}
38
39/**
40 * Determines whether a non-Literal node is a negative number that should be
41 * treated as if it were a single Literal node.
42 * @param {ASTNode} node Node to test.
43 * @returns {boolean} True if the node is a negative number that looks like a
44 * real literal and should be treated as such.
45 */
46function looksLikeLiteral(node) {
47 return (node.type === "UnaryExpression" &&
48 node.operator === "-" &&
49 node.prefix &&
50 node.argument.type === "Literal" &&
51 typeof node.argument.value === "number");
52}
53
54/**
55 * Attempts to derive a Literal node from nodes that are treated like literals.
56 * @param {ASTNode} node Node to normalize.
57 * @returns {ASTNode} The original node if the node is already a Literal, or a
58 * normalized Literal node with the negative number as the
59 * value if the node represents a negative number literal,
60 * otherwise null if the node cannot be converted to a
61 * normalized literal.
62 */
63function getNormalizedLiteral(node) {
64 if (node.type === "Literal") {
65 return node;
66 }
67
68 if (looksLikeLiteral(node)) {
69 return {
70 type: "Literal",
71 value: -node.argument.value,
72 raw: `-${node.argument.value}`
73 };
74 }
75
76 return null;
77}
78
79/**
80 * Checks whether two expressions reference the same value. For example:
81 * a = a
82 * a.b = a.b
83 * a[0] = a[0]
84 * a['b'] = a['b']
85 * @param {ASTNode} a Left side of the comparison.
86 * @param {ASTNode} b Right side of the comparison.
87 * @returns {boolean} True if both sides match and reference the same value.
88 */
89function same(a, b) {
90 if (a.type !== b.type) {
91 return false;
92 }
93
94 switch (a.type) {
95 case "Identifier":
96 return a.name === b.name;
97
98 case "Literal":
99 return a.value === b.value;
100
101 case "MemberExpression":
102
103 // x[0] = x[0]
104 // x[y] = x[y]
105 // x.y = x.y
106 return same(a.object, b.object) && same(a.property, b.property);
107
108 case "ThisExpression":
109 return true;
110
111 default:
112 return false;
113 }
114}
115
116//------------------------------------------------------------------------------
117// Rule Definition
118//------------------------------------------------------------------------------
119
120module.exports = {
121 meta: {
122 docs: {
123 description: "require or disallow \"Yoda\" conditions",
124 category: "Best Practices",
125 recommended: false
126 },
127
128 schema: [
129 {
130 enum: ["always", "never"]
131 },
132 {
133 type: "object",
134 properties: {
135 exceptRange: {
136 type: "boolean"
137 },
138 onlyEquality: {
139 type: "boolean"
140 }
141 },
142 additionalProperties: false
143 }
144 ]
145 },
146
147 create(context) {
148
149 // Default to "never" (!always) if no option
150 const always = (context.options[0] === "always");
151 const exceptRange = (context.options[1] && context.options[1].exceptRange);
152 const onlyEquality = (context.options[1] && context.options[1].onlyEquality);
153
154 const sourceCode = context.getSourceCode();
155
156 /**
157 * Determines whether node represents a range test.
158 * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
159 * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
160 * both operators must be `<` or `<=`. Finally, the literal on the left side
161 * must be less than or equal to the literal on the right side so that the
162 * test makes any sense.
163 * @param {ASTNode} node LogicalExpression node to test.
164 * @returns {boolean} Whether node is a range test.
165 */
166 function isRangeTest(node) {
167 const left = node.left,
168 right = node.right;
169
170 /**
171 * Determines whether node is of the form `0 <= x && x < 1`.
172 * @returns {boolean} Whether node is a "between" range test.
173 */
174 function isBetweenTest() {
175 let leftLiteral, rightLiteral;
176
177 return (node.operator === "&&" &&
178 (leftLiteral = getNormalizedLiteral(left.left)) &&
179 (rightLiteral = getNormalizedLiteral(right.right)) &&
180 leftLiteral.value <= rightLiteral.value &&
181 same(left.right, right.left));
182 }
183
184 /**
185 * Determines whether node is of the form `x < 0 || 1 <= x`.
186 * @returns {boolean} Whether node is an "outside" range test.
187 */
188 function isOutsideTest() {
189 let leftLiteral, rightLiteral;
190
191 return (node.operator === "||" &&
192 (leftLiteral = getNormalizedLiteral(left.right)) &&
193 (rightLiteral = getNormalizedLiteral(right.left)) &&
194 leftLiteral.value <= rightLiteral.value &&
195 same(left.left, right.right));
196 }
197
198 /**
199 * Determines whether node is wrapped in parentheses.
200 * @returns {boolean} Whether node is preceded immediately by an open
201 * paren token and followed immediately by a close
202 * paren token.
203 */
204 function isParenWrapped() {
205 let tokenBefore, tokenAfter;
206
207 return ((tokenBefore = sourceCode.getTokenBefore(node)) &&
208 tokenBefore.value === "(" &&
209 (tokenAfter = sourceCode.getTokenAfter(node)) &&
210 tokenAfter.value === ")");
211 }
212
213 return (node.type === "LogicalExpression" &&
214 left.type === "BinaryExpression" &&
215 right.type === "BinaryExpression" &&
216 isRangeTestOperator(left.operator) &&
217 isRangeTestOperator(right.operator) &&
218 (isBetweenTest() || isOutsideTest()) &&
219 isParenWrapped());
220 }
221
222 //--------------------------------------------------------------------------
223 // Public
224 //--------------------------------------------------------------------------
225
226 return {
227 BinaryExpression: always ? function(node) {
228
229 // Comparisons must always be yoda-style: if ("blue" === color)
230 if (
231 (node.right.type === "Literal" || looksLikeLiteral(node.right)) &&
232 !(node.left.type === "Literal" || looksLikeLiteral(node.left)) &&
233 !(!isEqualityOperator(node.operator) && onlyEquality) &&
234 isComparisonOperator(node.operator) &&
235 !(exceptRange && isRangeTest(context.getAncestors().pop()))
236 ) {
237 context.report({
238 node,
239 message: "Expected literal to be on the left side of {{operator}}.",
240 data: {
241 operator: node.operator
242 }
243 });
244 }
245
246 } : function(node) {
247
248 // Comparisons must never be yoda-style (default)
249 if (
250 (node.left.type === "Literal" || looksLikeLiteral(node.left)) &&
251 !(node.right.type === "Literal" || looksLikeLiteral(node.right)) &&
252 !(!isEqualityOperator(node.operator) && onlyEquality) &&
253 isComparisonOperator(node.operator) &&
254 !(exceptRange && isRangeTest(context.getAncestors().pop()))
255 ) {
256 context.report({
257 node,
258 message: "Expected literal to be on the right side of {{operator}}.",
259 data: {
260 operator: node.operator
261 }
262 });
263 }
264
265 }
266 };
267
268 }
269};