2 * @fileoverview Rule to require or disallow yoda comparisons
3 * @author Nicholas C. Zakas
7 //--------------------------------------------------------------------------
9 //--------------------------------------------------------------------------
11 const astUtils = require("./utils/ast-utils");
13 //--------------------------------------------------------------------------
15 //--------------------------------------------------------------------------
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.
22 function isComparisonOperator(operator) {
23 return (/^(==|===|!=|!==|<|>|<=|>=)$/u).test(operator);
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.
31 function isEqualityOperator(operator) {
32 return (/^(==|===)$/u).test(operator);
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.
41 function isRangeTestOperator(operator) {
42 return ["<", "<="].indexOf(operator) >= 0;
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.
52 function looksLikeLiteral(node) {
53 return (node.type === "UnaryExpression" &&
54 node.operator === "-" &&
56 node.argument.type === "Literal" &&
57 typeof node.argument.value === "number");
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
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`.
72 function getNormalizedLiteral(node, defaultValue) {
73 if (node.type === "Literal") {
77 if (looksLikeLiteral(node)) {
80 value: -node.argument.value,
81 raw: `-${node.argument.value}`
89 raw: String(defaultValue)
97 * Checks whether two expressions reference the same value. For example:
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.
106 function same(a, b) {
107 if (a.type !== b.type) {
113 return a.name === b.name;
116 return a.value === b.value;
118 case "MemberExpression": {
119 const nameA = astUtils.getStaticPropertyName(a);
122 if (nameA !== null) {
124 same(a.object, b.object) &&
125 nameA === astUtils.getStaticPropertyName(b)
135 a.computed === b.computed &&
136 same(a.object, b.object) &&
137 same(a.property, b.property)
141 case "ThisExpression":
149 //------------------------------------------------------------------------------
151 //------------------------------------------------------------------------------
158 description: "require or disallow \"Yoda\" conditions",
159 category: "Best Practices",
161 url: "https://eslint.org/docs/rules/yoda"
166 enum: ["always", "never"]
180 additionalProperties: false
186 expected: "Expected literal to be on the {{expectedSide}} side of {{operator}}."
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);
197 const sourceCode = context.getSourceCode();
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.
209 function isRangeTest(node) {
210 const left = node.left,
214 * Determines whether node is of the form `0 <= x && x < 1`.
215 * @returns {boolean} Whether node is a "between" range test.
217 function isBetweenTest() {
218 let leftLiteral, rightLiteral;
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));
228 * Determines whether node is of the form `x < 0 || 1 <= x`.
229 * @returns {boolean} Whether node is an "outside" range test.
231 function isOutsideTest() {
232 let leftLiteral, rightLiteral;
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));
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
247 function isParenWrapped() {
248 return astUtils.isParenthesised(sourceCode, node);
251 return (node.type === "LogicalExpression" &&
252 left.type === "BinaryExpression" &&
253 right.type === "BinaryExpression" &&
254 isRangeTestOperator(left.operator) &&
255 isRangeTestOperator(right.operator) &&
256 (isBetweenTest() || isOutsideTest()) &&
260 const OPERATOR_FLIP_MAP = {
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
276 function getFlippedString(node) {
277 const tokenBefore = sourceCode.getTokenBefore(node);
278 const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
279 const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]);
280 const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]);
281 const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
282 const firstRightToken = sourceCode.getTokenAfter(operatorToken);
283 const rightText = sourceCode.getText().slice(firstRightToken.range[0], node.range[1]);
287 if (tokenBefore && tokenBefore.range[1] === node.range[0] &&
288 !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)) {
292 return prefix + rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
295 //--------------------------------------------------------------------------
297 //--------------------------------------------------------------------------
300 BinaryExpression(node) {
301 const expectedLiteral = always ? node.left : node.right;
302 const expectedNonLiteral = always ? node.right : node.left;
304 // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
306 (expectedNonLiteral.type === "Literal" || looksLikeLiteral(expectedNonLiteral)) &&
307 !(expectedLiteral.type === "Literal" || looksLikeLiteral(expectedLiteral)) &&
308 !(!isEqualityOperator(node.operator) && onlyEquality) &&
309 isComparisonOperator(node.operator) &&
310 !(exceptRange && isRangeTest(context.getAncestors().pop()))
314 messageId: "expected",
316 operator: node.operator,
317 expectedSide: always ? "left" : "right"
319 fix: fixer => fixer.replaceText(node, getFlippedString(node))