UNPKG

11.1 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 /*
130 * x[0] = x[0]
131 * x[y] = x[y]
132 * x.y = x.y
133 */
134 return (
135 a.computed === b.computed &&
136 same(a.object, b.object) &&
137 same(a.property, b.property)
138 );
139 }
140
141 case "ThisExpression":
142 return true;
143
144 default:
145 return false;
146 }
147}
148
149//------------------------------------------------------------------------------
150// Rule Definition
151//------------------------------------------------------------------------------
152
153module.exports = {
154 meta: {
155 docs: {
156 description: "require or disallow \"Yoda\" conditions",
157 category: "Best Practices",
158 recommended: false,
159 url: "https://eslint.org/docs/rules/yoda"
160 },
161
162 schema: [
163 {
164 enum: ["always", "never"]
165 },
166 {
167 type: "object",
168 properties: {
169 exceptRange: {
170 type: "boolean"
171 },
172 onlyEquality: {
173 type: "boolean"
174 }
175 },
176 additionalProperties: false
177 }
178 ],
179
180 fixable: "code"
181 },
182
183 create(context) {
184
185 // Default to "never" (!always) if no option
186 const always = (context.options[0] === "always");
187 const exceptRange = (context.options[1] && context.options[1].exceptRange);
188 const onlyEquality = (context.options[1] && context.options[1].onlyEquality);
189
190 const sourceCode = context.getSourceCode();
191
192 /**
193 * Determines whether node represents a range test.
194 * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
195 * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
196 * both operators must be `<` or `<=`. Finally, the literal on the left side
197 * must be less than or equal to the literal on the right side so that the
198 * test makes any sense.
199 * @param {ASTNode} node LogicalExpression node to test.
200 * @returns {boolean} Whether node is a range test.
201 */
202 function isRangeTest(node) {
203 const left = node.left,
204 right = node.right;
205
206 /**
207 * Determines whether node is of the form `0 <= x && x < 1`.
208 * @returns {boolean} Whether node is a "between" range test.
209 */
210 function isBetweenTest() {
211 let leftLiteral, rightLiteral;
212
213 return (node.operator === "&&" &&
214 (leftLiteral = getNormalizedLiteral(left.left)) &&
215 (rightLiteral = getNormalizedLiteral(right.right, Number.POSITIVE_INFINITY)) &&
216 leftLiteral.value <= rightLiteral.value &&
217 same(left.right, right.left));
218 }
219
220 /**
221 * Determines whether node is of the form `x < 0 || 1 <= x`.
222 * @returns {boolean} Whether node is an "outside" range test.
223 */
224 function isOutsideTest() {
225 let leftLiteral, rightLiteral;
226
227 return (node.operator === "||" &&
228 (leftLiteral = getNormalizedLiteral(left.right, Number.NEGATIVE_INFINITY)) &&
229 (rightLiteral = getNormalizedLiteral(right.left)) &&
230 leftLiteral.value <= rightLiteral.value &&
231 same(left.left, right.right));
232 }
233
234 /**
235 * Determines whether node is wrapped in parentheses.
236 * @returns {boolean} Whether node is preceded immediately by an open
237 * paren token and followed immediately by a close
238 * paren token.
239 */
240 function isParenWrapped() {
241 return astUtils.isParenthesised(sourceCode, node);
242 }
243
244 return (node.type === "LogicalExpression" &&
245 left.type === "BinaryExpression" &&
246 right.type === "BinaryExpression" &&
247 isRangeTestOperator(left.operator) &&
248 isRangeTestOperator(right.operator) &&
249 (isBetweenTest() || isOutsideTest()) &&
250 isParenWrapped());
251 }
252
253 const OPERATOR_FLIP_MAP = {
254 "===": "===",
255 "!==": "!==",
256 "==": "==",
257 "!=": "!=",
258 "<": ">",
259 ">": "<",
260 "<=": ">=",
261 ">=": "<="
262 };
263
264 /**
265 * Returns a string representation of a BinaryExpression node with its sides/operator flipped around.
266 * @param {ASTNode} node The BinaryExpression node
267 * @returns {string} A string representation of the node with the sides and operator flipped
268 */
269 function getFlippedString(node) {
270 const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
271 const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]);
272 const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]);
273 const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
274 const rightText = sourceCode.getText().slice(sourceCode.getTokenAfter(operatorToken).range[0], node.range[1]);
275
276 return rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
277 }
278
279 //--------------------------------------------------------------------------
280 // Public
281 //--------------------------------------------------------------------------
282
283 return {
284 BinaryExpression(node) {
285 const expectedLiteral = always ? node.left : node.right;
286 const expectedNonLiteral = always ? node.right : node.left;
287
288 // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
289 if (
290 (expectedNonLiteral.type === "Literal" || looksLikeLiteral(expectedNonLiteral)) &&
291 !(expectedLiteral.type === "Literal" || looksLikeLiteral(expectedLiteral)) &&
292 !(!isEqualityOperator(node.operator) && onlyEquality) &&
293 isComparisonOperator(node.operator) &&
294 !(exceptRange && isRangeTest(context.getAncestors().pop()))
295 ) {
296 context.report({
297 node,
298 message: "Expected literal to be on the {{expectedSide}} side of {{operator}}.",
299 data: {
300 operator: node.operator,
301 expectedSide: always ? "left" : "right"
302 },
303 fix: fixer => fixer.replaceText(node, getFlippedString(node))
304 });
305 }
306
307 }
308 };
309
310 }
311};