b00acf82c702b07b36ae9d7af92a74f8e01d6c70
[dotfiles/.git] / yoda.js
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
11 const 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  */
22 function 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  */
31 function 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  */
41 function 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  */
52 function 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  */
72 function 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  */
106 function 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 !== null) {
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
153 module.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 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]);
284
285             let prefix = "";
286
287             if (tokenBefore && tokenBefore.range[1] === node.range[0] &&
288                     !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)) {
289                 prefix = " ";
290             }
291
292             return prefix + rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
293         }
294
295         //--------------------------------------------------------------------------
296         // Public
297         //--------------------------------------------------------------------------
298
299         return {
300             BinaryExpression(node) {
301                 const expectedLiteral = always ? node.left : node.right;
302                 const expectedNonLiteral = always ? node.right : node.left;
303
304                 // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
305                 if (
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()))
311                 ) {
312                     context.report({
313                         node,
314                         messageId: "expected",
315                         data: {
316                             operator: node.operator,
317                             expectedSide: always ? "left" : "right"
318                         },
319                         fix: fixer => fixer.replaceText(node, getFlippedString(node))
320                     });
321                 }
322
323             }
324         };
325
326     }
327 };