UNPKG

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