UNPKG

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