]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/no-constant-binary-expression.js
dccfa2f582640a9919bf60f81205f48925566511
[pve-eslint.git] / eslint / lib / rules / no-constant-binary-expression.js
1 /**
2 * @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit
3 * @author Jordan Eldredge <https://jordaneldredge.com>
4 */
5
6 "use strict";
7
8 const globals = require("globals");
9 const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");
10
11 const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
12
13 //------------------------------------------------------------------------------
14 // Helpers
15 //------------------------------------------------------------------------------
16
17 /**
18 * Test if an AST node has a statically knowable constant nullishness. Meaning,
19 * it will always resolve to a constant value of either: `null`, `undefined`
20 * or not `null` _or_ `undefined`. An expression that can vary between those
21 * three states at runtime would return `false`.
22 * @param {Scope} scope The scope in which the node was found.
23 * @param {ASTNode} node The AST node being tested.
24 * @returns {boolean} Does `node` have constant nullishness?
25 */
26 function hasConstantNullishness(scope, node) {
27 switch (node.type) {
28 case "ObjectExpression": // Objects are never nullish
29 case "ArrayExpression": // Arrays are never nullish
30 case "ArrowFunctionExpression": // Functions never nullish
31 case "FunctionExpression": // Functions are never nullish
32 case "ClassExpression": // Classes are never nullish
33 case "NewExpression": // Objects are never nullish
34 case "Literal": // Nullish, or non-nullish, literals never change
35 case "TemplateLiteral": // A string is never nullish
36 case "UpdateExpression": // Numbers are never nullish
37 case "BinaryExpression": // Numbers, strings, or booleans are never nullish
38 return true;
39 case "CallExpression": {
40 if (node.callee.type !== "Identifier") {
41 return false;
42 }
43 const functionName = node.callee.name;
44
45 return (functionName === "Boolean" || functionName === "String" || functionName === "Number") &&
46 isReferenceToGlobalVariable(scope, node.callee);
47 }
48 case "AssignmentExpression":
49 if (node.operator === "=") {
50 return hasConstantNullishness(scope, node.right);
51 }
52
53 /*
54 * Handling short-circuiting assignment operators would require
55 * walking the scope. We won't attempt that (for now...) /
56 */
57 if (isLogicalAssignmentOperator(node.operator)) {
58 return false;
59 }
60
61 /*
62 * The remaining assignment expressions all result in a numeric or
63 * string (non-nullish) value:
64 * "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
65 */
66
67 return true;
68 case "UnaryExpression":
69
70 /*
71 * "void" Always returns `undefined`
72 * "typeof" All types are strings, and thus non-nullish
73 * "!" Boolean is never nullish
74 * "delete" Returns a boolean, which is never nullish
75 * Math operators always return numbers or strings, neither of which
76 * are non-nullish "+", "-", "~"
77 */
78
79 return true;
80 case "SequenceExpression": {
81 const last = node.expressions[node.expressions.length - 1];
82
83 return hasConstantNullishness(scope, last);
84 }
85 case "Identifier":
86 return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
87 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
88 case "JSXFragment":
89 return false;
90 default:
91 return false;
92 }
93 }
94
95 /**
96 * Test if an AST node is a boolean value that never changes. Specifically we
97 * test for:
98 * 1. Literal booleans (`true` or `false`)
99 * 2. Unary `!` expressions with a constant value
100 * 3. Constant booleans created via the `Boolean` global function
101 * @param {Scope} scope The scope in which the node was found.
102 * @param {ASTNode} node The node to test
103 * @returns {boolean} Is `node` guaranteed to be a boolean?
104 */
105 function isStaticBoolean(scope, node) {
106 switch (node.type) {
107 case "Literal":
108 return typeof node.value === "boolean";
109 case "CallExpression":
110 return node.callee.type === "Identifier" && node.callee.name === "Boolean" &&
111 isReferenceToGlobalVariable(scope, node.callee) &&
112 (node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
113 case "UnaryExpression":
114 return node.operator === "!" && isConstant(scope, node.argument, true);
115 default:
116 return false;
117 }
118 }
119
120
121 /**
122 * Test if an AST node will always give the same result when compared to a
123 * boolean value. Note that comparison to boolean values is different than
124 * truthiness.
125 * https://262.ecma-international.org/5.1/#sec-11.9.3
126 *
127 * Javascript `==` operator works by converting the boolean to `1` (true) or
128 * `+0` (false) and then checks the values `==` equality to that number.
129 * @param {Scope} scope The scope in which node was found.
130 * @param {ASTNode} node The node to test.
131 * @returns {boolean} Will `node` always coerce to the same boolean value?
132 */
133 function hasConstantLooseBooleanComparison(scope, node) {
134 switch (node.type) {
135 case "ObjectExpression":
136 case "ClassExpression":
137
138 /**
139 * In theory objects like:
140 *
141 * `{toString: () => a}`
142 * `{valueOf: () => a}`
143 *
144 * Or a classes like:
145 *
146 * `class { static toString() { return a } }`
147 * `class { static valueOf() { return a } }`
148 *
149 * Are not constant verifiably when `inBooleanPosition` is
150 * false, but it's an edge case we've opted not to handle.
151 */
152 return true;
153 case "ArrayExpression": {
154 const nonSpreadElements = node.elements.filter(e =>
155
156 // Elements can be `null` in sparse arrays: `[,,]`;
157 e !== null && e.type !== "SpreadElement");
158
159
160 /*
161 * Possible future direction if needed: We could check if the
162 * single value would result in variable boolean comparison.
163 * For now we will err on the side of caution since `[x]` could
164 * evaluate to `[0]` or `[1]`.
165 */
166 return node.elements.length === 0 || nonSpreadElements.length > 1;
167 }
168 case "ArrowFunctionExpression":
169 case "FunctionExpression":
170 return true;
171 case "UnaryExpression":
172 if (node.operator === "void" || // Always returns `undefined`
173 node.operator === "typeof" // All `typeof` strings, when coerced to number, are not 0 or 1.
174 ) {
175 return true;
176 }
177 if (node.operator === "!") {
178 return isConstant(scope, node.argument, true);
179 }
180
181 /*
182 * We won't try to reason about +, -, ~, or delete
183 * In theory, for the mathematical operators, we could look at the
184 * argument and try to determine if it coerces to a constant numeric
185 * value.
186 */
187 return false;
188 case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.
189 return false;
190 case "CallExpression": {
191 if (node.callee.type === "Identifier" &&
192 node.callee.name === "Boolean" &&
193 isReferenceToGlobalVariable(scope, node.callee)
194 ) {
195 return node.arguments.length === 0 || isConstant(scope, node.arguments[0], true);
196 }
197 return false;
198 }
199 case "Literal": // True or false, literals never change
200 return true;
201 case "Identifier":
202 return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
203 case "TemplateLiteral":
204
205 /*
206 * In theory we could try to check if the quasi are sufficient to
207 * prove that the expression will always be true, but it would be
208 * tricky to get right. For example: `000.${foo}000`
209 */
210 return node.expressions.length === 0;
211 case "AssignmentExpression":
212 if (node.operator === "=") {
213 return hasConstantLooseBooleanComparison(scope, node.right);
214 }
215
216 /*
217 * Handling short-circuiting assignment operators would require
218 * walking the scope. We won't attempt that (for now...)
219 *
220 * The remaining assignment expressions all result in a numeric or
221 * string (non-nullish) values which could be truthy or falsy:
222 * "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
223 */
224 return false;
225 case "SequenceExpression": {
226 const last = node.expressions[node.expressions.length - 1];
227
228 return hasConstantLooseBooleanComparison(scope, last);
229 }
230 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
231 case "JSXFragment":
232 return false;
233 default:
234 return false;
235 }
236 }
237
238
239 /**
240 * Test if an AST node will always give the same result when _strictly_ compared
241 * to a boolean value. This can happen if the expression can never be boolean, or
242 * if it is always the same boolean value.
243 * @param {Scope} scope The scope in which the node was found.
244 * @param {ASTNode} node The node to test
245 * @returns {boolean} Will `node` always give the same result when compared to a
246 * static boolean value?
247 */
248 function hasConstantStrictBooleanComparison(scope, node) {
249 switch (node.type) {
250 case "ObjectExpression": // Objects are not booleans
251 case "ArrayExpression": // Arrays are not booleans
252 case "ArrowFunctionExpression": // Functions are not booleans
253 case "FunctionExpression":
254 case "ClassExpression": // Classes are not booleans
255 case "NewExpression": // Objects are not booleans
256 case "TemplateLiteral": // Strings are not booleans
257 case "Literal": // True, false, or not boolean, literals never change.
258 case "UpdateExpression": // Numbers are not booleans
259 return true;
260 case "BinaryExpression":
261 return NUMERIC_OR_STRING_BINARY_OPERATORS.has(node.operator);
262 case "UnaryExpression": {
263 if (node.operator === "delete") {
264 return false;
265 }
266 if (node.operator === "!") {
267 return isConstant(scope, node.argument, true);
268 }
269
270 /*
271 * The remaining operators return either strings or numbers, neither
272 * of which are boolean.
273 */
274 return true;
275 }
276 case "SequenceExpression": {
277 const last = node.expressions[node.expressions.length - 1];
278
279 return hasConstantStrictBooleanComparison(scope, last);
280 }
281 case "Identifier":
282 return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
283 case "AssignmentExpression":
284 if (node.operator === "=") {
285 return hasConstantStrictBooleanComparison(scope, node.right);
286 }
287
288 /*
289 * Handling short-circuiting assignment operators would require
290 * walking the scope. We won't attempt that (for now...)
291 */
292 if (isLogicalAssignmentOperator(node.operator)) {
293 return false;
294 }
295
296 /*
297 * The remaining assignment expressions all result in either a number
298 * or a string, neither of which can ever be boolean.
299 */
300 return true;
301 case "CallExpression": {
302 if (node.callee.type !== "Identifier") {
303 return false;
304 }
305 const functionName = node.callee.name;
306
307 if (
308 (functionName === "String" || functionName === "Number") &&
309 isReferenceToGlobalVariable(scope, node.callee)
310 ) {
311 return true;
312 }
313 if (functionName === "Boolean" && isReferenceToGlobalVariable(scope, node.callee)) {
314 return (
315 node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
316 }
317 return false;
318 }
319 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
320 case "JSXFragment":
321 return false;
322 default:
323 return false;
324 }
325 }
326
327 /**
328 * Test if an AST node will always result in a newly constructed object
329 * @param {Scope} scope The scope in which the node was found.
330 * @param {ASTNode} node The node to test
331 * @returns {boolean} Will `node` always be new?
332 */
333 function isAlwaysNew(scope, node) {
334 switch (node.type) {
335 case "ObjectExpression":
336 case "ArrayExpression":
337 case "ArrowFunctionExpression":
338 case "FunctionExpression":
339 case "ClassExpression":
340 return true;
341 case "NewExpression": {
342 if (node.callee.type !== "Identifier") {
343 return false;
344 }
345
346 /*
347 * All the built-in constructors are always new, but
348 * user-defined constructors could return a sentinel
349 * object.
350 *
351 * Catching these is especially useful for primitive constructures
352 * which return boxed values, a surprising gotcha' in JavaScript.
353 */
354 return Object.hasOwnProperty.call(globals.builtin, node.callee.name) &&
355 isReferenceToGlobalVariable(scope, node.callee);
356 }
357 case "Literal":
358
359 // Regular expressions are objects, and thus always new
360 return typeof node.regex === "object";
361 case "SequenceExpression": {
362 const last = node.expressions[node.expressions.length - 1];
363
364 return isAlwaysNew(scope, last);
365 }
366 case "AssignmentExpression":
367 if (node.operator === "=") {
368 return isAlwaysNew(scope, node.right);
369 }
370 return false;
371 case "ConditionalExpression":
372 return isAlwaysNew(scope, node.consequent) && isAlwaysNew(scope, node.alternate);
373 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
374 case "JSXFragment":
375 return false;
376 default:
377 return false;
378 }
379 }
380
381 /**
382 * Checks whether or not a node is `null` or `undefined`. Similar to the one
383 * found in ast-utils.js, but this one correctly handles the edge case that
384 * `undefined` has been redefined.
385 * @param {Scope} scope Scope in which the expression was found.
386 * @param {ASTNode} node A node to check.
387 * @returns {boolean} Whether or not the node is a `null` or `undefined`.
388 * @public
389 */
390 function isNullOrUndefined(scope, node) {
391 return (
392 isNullLiteral(node) ||
393 (node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
394 (node.type === "UnaryExpression" && node.operator === "void")
395 );
396 }
397
398
399 /**
400 * Checks if one operand will cause the result to be constant.
401 * @param {Scope} scope Scope in which the expression was found.
402 * @param {ASTNode} a One side of the expression
403 * @param {ASTNode} b The other side of the expression
404 * @param {string} operator The binary expression operator
405 * @returns {ASTNode | null} The node which will cause the expression to have a constant result.
406 */
407 function findBinaryExpressionConstantOperand(scope, a, b, operator) {
408 if (operator === "==" || operator === "!=") {
409 if (
410 (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
411 (isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))
412 ) {
413 return b;
414 }
415 } else if (operator === "===" || operator === "!==") {
416 if (
417 (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
418 (isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))
419 ) {
420 return b;
421 }
422 }
423 return null;
424 }
425
426 //------------------------------------------------------------------------------
427 // Rule Definition
428 //------------------------------------------------------------------------------
429
430 /** @type {import('../shared/types').Rule} */
431 module.exports = {
432 meta: {
433 type: "problem",
434 docs: {
435 description: "Disallow expressions where the operation doesn't affect the value",
436 recommended: false,
437 url: "https://eslint.org/docs/rules/no-constant-binary-expression"
438 },
439 schema: [],
440 messages: {
441 constantBinaryOperand: "Unexpected constant binary expression. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.",
442 constantShortCircuit: "Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression.",
443 alwaysNew: "Unexpected comparison to newly constructed object. These two values can never be equal.",
444 bothAlwaysNew: "Unexpected comparison of two newly constructed objects. These two values can never be equal."
445 }
446 },
447
448 create(context) {
449 return {
450 LogicalExpression(node) {
451 const { operator, left } = node;
452 const scope = context.getScope();
453
454 if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {
455 context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });
456 } else if (operator === "??" && hasConstantNullishness(scope, left)) {
457 context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });
458 }
459 },
460 BinaryExpression(node) {
461 const scope = context.getScope();
462 const { right, left, operator } = node;
463 const rightConstantOperand = findBinaryExpressionConstantOperand(scope, left, right, operator);
464 const leftConstantOperand = findBinaryExpressionConstantOperand(scope, right, left, operator);
465
466 if (rightConstantOperand) {
467 context.report({ node: rightConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "left" } });
468 } else if (leftConstantOperand) {
469 context.report({ node: leftConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "right" } });
470 } else if (operator === "===" || operator === "!==") {
471 if (isAlwaysNew(scope, left)) {
472 context.report({ node: left, messageId: "alwaysNew" });
473 } else if (isAlwaysNew(scope, right)) {
474 context.report({ node: right, messageId: "alwaysNew" });
475 }
476 } else if (operator === "==" || operator === "!=") {
477
478 /*
479 * If both sides are "new", then both sides are objects and
480 * therefore they will be compared by reference even with `==`
481 * equality.
482 */
483 if (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {
484 context.report({ node: left, messageId: "bothAlwaysNew" });
485 }
486 }
487
488 }
489
490 /*
491 * In theory we could handle short-circuiting assignment operators,
492 * for some constant values, but that would require walking the
493 * scope to find the value of the variable being assigned. This is
494 * dependant on https://github.com/eslint/eslint/issues/13776
495 *
496 * AssignmentExpression() {},
497 */
498 };
499 }
500 };