]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/yoda.js
import 8.3.0 source
[pve-eslint.git] / eslint / lib / rules / 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 isNegativeNumericLiteral(node) {
53 return (
54 node.type === "UnaryExpression" &&
55 node.operator === "-" &&
56 node.prefix &&
57 astUtils.isNumericLiteral(node.argument)
58 );
59 }
60
61 /**
62 * Determines whether a node is a Template Literal which can be determined statically.
63 * @param {ASTNode} node Node to test
64 * @returns {boolean} True if the node is a Template Literal without expression.
65 */
66 function isStaticTemplateLiteral(node) {
67 return node.type === "TemplateLiteral" && node.expressions.length === 0;
68 }
69
70 /**
71 * Determines whether a non-Literal node should be treated as a single Literal node.
72 * @param {ASTNode} node Node to test
73 * @returns {boolean} True if the node should be treated as a single Literal node.
74 */
75 function looksLikeLiteral(node) {
76 return isNegativeNumericLiteral(node) || isStaticTemplateLiteral(node);
77 }
78
79 /**
80 * Attempts to derive a Literal node from nodes that are treated like literals.
81 * @param {ASTNode} node Node to normalize.
82 * @returns {ASTNode} One of the following options.
83 * 1. The original node if the node is already a Literal
84 * 2. A normalized Literal node with the negative number as the value if the
85 * node represents a negative number literal.
86 * 3. A normalized Literal node with the string as the value if the node is
87 * a Template Literal without expression.
88 * 4. Otherwise `null`.
89 */
90 function getNormalizedLiteral(node) {
91 if (node.type === "Literal") {
92 return node;
93 }
94
95 if (isNegativeNumericLiteral(node)) {
96 return {
97 type: "Literal",
98 value: -node.argument.value,
99 raw: `-${node.argument.value}`
100 };
101 }
102
103 if (isStaticTemplateLiteral(node)) {
104 return {
105 type: "Literal",
106 value: node.quasis[0].value.cooked,
107 raw: node.quasis[0].value.raw
108 };
109 }
110
111 return null;
112 }
113
114 //------------------------------------------------------------------------------
115 // Rule Definition
116 //------------------------------------------------------------------------------
117
118 module.exports = {
119 meta: {
120 type: "suggestion",
121
122 docs: {
123 description: 'require or disallow "Yoda" conditions',
124 recommended: false,
125 url: "https://eslint.org/docs/rules/yoda"
126 },
127
128 schema: [
129 {
130 enum: ["always", "never"]
131 },
132 {
133 type: "object",
134 properties: {
135 exceptRange: {
136 type: "boolean",
137 default: false
138 },
139 onlyEquality: {
140 type: "boolean",
141 default: false
142 }
143 },
144 additionalProperties: false
145 }
146 ],
147
148 fixable: "code",
149 messages: {
150 expected:
151 "Expected literal to be on the {{expectedSide}} side of {{operator}}."
152 }
153 },
154
155 create(context) {
156
157 // Default to "never" (!always) if no option
158 const always = context.options[0] === "always";
159 const exceptRange =
160 context.options[1] && context.options[1].exceptRange;
161 const onlyEquality =
162 context.options[1] && context.options[1].onlyEquality;
163
164 const sourceCode = context.getSourceCode();
165
166 /**
167 * Determines whether node represents a range test.
168 * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
169 * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
170 * both operators must be `<` or `<=`. Finally, the literal on the left side
171 * must be less than or equal to the literal on the right side so that the
172 * test makes any sense.
173 * @param {ASTNode} node LogicalExpression node to test.
174 * @returns {boolean} Whether node is a range test.
175 */
176 function isRangeTest(node) {
177 const left = node.left,
178 right = node.right;
179
180 /**
181 * Determines whether node is of the form `0 <= x && x < 1`.
182 * @returns {boolean} Whether node is a "between" range test.
183 */
184 function isBetweenTest() {
185 if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) {
186 const leftLiteral = getNormalizedLiteral(left.left);
187 const rightLiteral = getNormalizedLiteral(right.right);
188
189 if (leftLiteral === null && rightLiteral === null) {
190 return false;
191 }
192
193 if (rightLiteral === null || leftLiteral === null) {
194 return true;
195 }
196
197 if (leftLiteral.value <= rightLiteral.value) {
198 return true;
199 }
200 }
201 return false;
202 }
203
204 /**
205 * Determines whether node is of the form `x < 0 || 1 <= x`.
206 * @returns {boolean} Whether node is an "outside" range test.
207 */
208 function isOutsideTest() {
209 if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) {
210 const leftLiteral = getNormalizedLiteral(left.right);
211 const rightLiteral = getNormalizedLiteral(right.left);
212
213 if (leftLiteral === null && rightLiteral === null) {
214 return false;
215 }
216
217 if (rightLiteral === null || leftLiteral === null) {
218 return true;
219 }
220
221 if (leftLiteral.value <= rightLiteral.value) {
222 return true;
223 }
224 }
225
226 return false;
227 }
228
229 /**
230 * Determines whether node is wrapped in parentheses.
231 * @returns {boolean} Whether node is preceded immediately by an open
232 * paren token and followed immediately by a close
233 * paren token.
234 */
235 function isParenWrapped() {
236 return astUtils.isParenthesised(sourceCode, node);
237 }
238
239 return (
240 node.type === "LogicalExpression" &&
241 left.type === "BinaryExpression" &&
242 right.type === "BinaryExpression" &&
243 isRangeTestOperator(left.operator) &&
244 isRangeTestOperator(right.operator) &&
245 (isBetweenTest() || isOutsideTest()) &&
246 isParenWrapped()
247 );
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(
268 node.left,
269 node.right,
270 token => token.value === node.operator
271 );
272 const lastLeftToken = sourceCode.getTokenBefore(operatorToken);
273 const firstRightToken = sourceCode.getTokenAfter(operatorToken);
274
275 const source = sourceCode.getText();
276
277 const leftText = source.slice(
278 node.range[0],
279 lastLeftToken.range[1]
280 );
281 const textBeforeOperator = source.slice(
282 lastLeftToken.range[1],
283 operatorToken.range[0]
284 );
285 const textAfterOperator = source.slice(
286 operatorToken.range[1],
287 firstRightToken.range[0]
288 );
289 const rightText = source.slice(
290 firstRightToken.range[0],
291 node.range[1]
292 );
293
294 const tokenBefore = sourceCode.getTokenBefore(node);
295 const tokenAfter = sourceCode.getTokenAfter(node);
296 let prefix = "";
297 let suffix = "";
298
299 if (
300 tokenBefore &&
301 tokenBefore.range[1] === node.range[0] &&
302 !astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)
303 ) {
304 prefix = " ";
305 }
306
307 if (
308 tokenAfter &&
309 node.range[1] === tokenAfter.range[0] &&
310 !astUtils.canTokensBeAdjacent(lastLeftToken, tokenAfter)
311 ) {
312 suffix = " ";
313 }
314
315 return (
316 prefix +
317 rightText +
318 textBeforeOperator +
319 OPERATOR_FLIP_MAP[operatorToken.value] +
320 textAfterOperator +
321 leftText +
322 suffix
323 );
324 }
325
326 //--------------------------------------------------------------------------
327 // Public
328 //--------------------------------------------------------------------------
329
330 return {
331 BinaryExpression(node) {
332 const expectedLiteral = always ? node.left : node.right;
333 const expectedNonLiteral = always ? node.right : node.left;
334
335 // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
336 if (
337 (expectedNonLiteral.type === "Literal" ||
338 looksLikeLiteral(expectedNonLiteral)) &&
339 !(
340 expectedLiteral.type === "Literal" ||
341 looksLikeLiteral(expectedLiteral)
342 ) &&
343 !(!isEqualityOperator(node.operator) && onlyEquality) &&
344 isComparisonOperator(node.operator) &&
345 !(exceptRange && isRangeTest(context.getAncestors().pop()))
346 ) {
347 context.report({
348 node,
349 messageId: "expected",
350 data: {
351 operator: node.operator,
352 expectedSide: always ? "left" : "right"
353 },
354 fix: fixer =>
355 fixer.replaceText(node, getFlippedString(node))
356 });
357 }
358 }
359 };
360 }
361 };