2 * @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit
3 * @author Jordan Eldredge <https://jordaneldredge.com>
8 const globals
= require("globals");
9 const { isNullLiteral
, isConstant
, isReferenceToGlobalVariable
, isLogicalAssignmentOperator
} = require("./utils/ast-utils");
11 const NUMERIC_OR_STRING_BINARY_OPERATORS
= new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
13 //------------------------------------------------------------------------------
15 //------------------------------------------------------------------------------
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?
26 function hasConstantNullishness(scope
, node
) {
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
39 case "CallExpression": {
40 if (node
.callee
.type
!== "Identifier") {
43 const functionName
= node
.callee
.name
;
45 return (functionName
=== "Boolean" || functionName
=== "String" || functionName
=== "Number") &&
46 isReferenceToGlobalVariable(scope
, node
.callee
);
48 case "AssignmentExpression":
49 if (node
.operator
=== "=") {
50 return hasConstantNullishness(scope
, node
.right
);
54 * Handling short-circuiting assignment operators would require
55 * walking the scope. We won't attempt that (for now...) /
57 if (isLogicalAssignmentOperator(node
.operator
)) {
62 * The remaining assignment expressions all result in a numeric or
63 * string (non-nullish) value:
64 * "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
68 case "UnaryExpression":
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 "+", "-", "~"
80 case "SequenceExpression": {
81 const last
= node
.expressions
[node
.expressions
.length
- 1];
83 return hasConstantNullishness(scope
, last
);
86 return node
.name
=== "undefined" && isReferenceToGlobalVariable(scope
, node
);
87 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
96 * Test if an AST node is a boolean value that never changes. Specifically we
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?
105 function isStaticBoolean(scope
, node
) {
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);
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
125 * https://262.ecma-international.org/5.1/#sec-11.9.3
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?
133 function hasConstantLooseBooleanComparison(scope
, node
) {
135 case "ObjectExpression":
136 case "ClassExpression":
139 * In theory objects like:
141 * `{toString: () => a}`
142 * `{valueOf: () => a}`
146 * `class { static toString() { return a } }`
147 * `class { static valueOf() { return a } }`
149 * Are not constant verifiably when `inBooleanPosition` is
150 * false, but it's an edge case we've opted not to handle.
153 case "ArrayExpression": {
154 const nonSpreadElements
= node
.elements
.filter(e
=>
156 // Elements can be `null` in sparse arrays: `[,,]`;
157 e
!== null && e
.type
!== "SpreadElement");
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]`.
166 return node
.elements
.length
=== 0 || nonSpreadElements
.length
> 1;
168 case "ArrowFunctionExpression":
169 case "FunctionExpression":
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.
177 if (node
.operator
=== "!") {
178 return isConstant(scope
, node
.argument
, true);
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
188 case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.
190 case "CallExpression": {
191 if (node
.callee
.type
=== "Identifier" &&
192 node
.callee
.name
=== "Boolean" &&
193 isReferenceToGlobalVariable(scope
, node
.callee
)
195 return node
.arguments
.length
=== 0 || isConstant(scope
, node
.arguments
[0], true);
199 case "Literal": // True or false, literals never change
202 return node
.name
=== "undefined" && isReferenceToGlobalVariable(scope
, node
);
203 case "TemplateLiteral":
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`
210 return node
.expressions
.length
=== 0;
211 case "AssignmentExpression":
212 if (node
.operator
=== "=") {
213 return hasConstantLooseBooleanComparison(scope
, node
.right
);
217 * Handling short-circuiting assignment operators would require
218 * walking the scope. We won't attempt that (for now...)
220 * The remaining assignment expressions all result in a numeric or
221 * string (non-nullish) values which could be truthy or falsy:
222 * "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
225 case "SequenceExpression": {
226 const last
= node
.expressions
[node
.expressions
.length
- 1];
228 return hasConstantLooseBooleanComparison(scope
, last
);
230 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
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?
248 function hasConstantStrictBooleanComparison(scope
, node
) {
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
260 case "BinaryExpression":
261 return NUMERIC_OR_STRING_BINARY_OPERATORS
.has(node
.operator
);
262 case "UnaryExpression": {
263 if (node
.operator
=== "delete") {
266 if (node
.operator
=== "!") {
267 return isConstant(scope
, node
.argument
, true);
271 * The remaining operators return either strings or numbers, neither
272 * of which are boolean.
276 case "SequenceExpression": {
277 const last
= node
.expressions
[node
.expressions
.length
- 1];
279 return hasConstantStrictBooleanComparison(scope
, last
);
282 return node
.name
=== "undefined" && isReferenceToGlobalVariable(scope
, node
);
283 case "AssignmentExpression":
284 if (node
.operator
=== "=") {
285 return hasConstantStrictBooleanComparison(scope
, node
.right
);
289 * Handling short-circuiting assignment operators would require
290 * walking the scope. We won't attempt that (for now...)
292 if (isLogicalAssignmentOperator(node
.operator
)) {
297 * The remaining assignment expressions all result in either a number
298 * or a string, neither of which can ever be boolean.
301 case "CallExpression": {
302 if (node
.callee
.type
!== "Identifier") {
305 const functionName
= node
.callee
.name
;
308 (functionName
=== "String" || functionName
=== "Number") &&
309 isReferenceToGlobalVariable(scope
, node
.callee
)
313 if (functionName
=== "Boolean" && isReferenceToGlobalVariable(scope
, node
.callee
)) {
315 node
.arguments
.length
=== 0 || isConstant(scope
, node
.arguments
[0], true));
319 case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
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?
333 function isAlwaysNew(scope
, node
) {
335 case "ObjectExpression":
336 case "ArrayExpression":
337 case "ArrowFunctionExpression":
338 case "FunctionExpression":
339 case "ClassExpression":
341 case "NewExpression": {
342 if (node
.callee
.type
!== "Identifier") {
347 * All the built-in constructors are always new, but
348 * user-defined constructors could return a sentinel
351 * Catching these is especially useful for primitive constructures
352 * which return boxed values, a surprising gotcha' in JavaScript.
354 return Object
.hasOwnProperty
.call(globals
.builtin
, node
.callee
.name
) &&
355 isReferenceToGlobalVariable(scope
, node
.callee
);
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];
364 return isAlwaysNew(scope
, last
);
366 case "AssignmentExpression":
367 if (node
.operator
=== "=") {
368 return isAlwaysNew(scope
, node
.right
);
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.
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`.
390 function isNullOrUndefined(scope
, node
) {
392 isNullLiteral(node
) ||
393 (node
.type
=== "Identifier" && node
.name
=== "undefined" && isReferenceToGlobalVariable(scope
, node
)) ||
394 (node
.type
=== "UnaryExpression" && node
.operator
=== "void")
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.
407 function findBinaryExpressionConstantOperand(scope
, a
, b
, operator
) {
408 if (operator
=== "==" || operator
=== "!=") {
410 (isNullOrUndefined(scope
, a
) && hasConstantNullishness(scope
, b
)) ||
411 (isStaticBoolean(scope
, a
) && hasConstantLooseBooleanComparison(scope
, b
))
415 } else if (operator
=== "===" || operator
=== "!==") {
417 (isNullOrUndefined(scope
, a
) && hasConstantNullishness(scope
, b
)) ||
418 (isStaticBoolean(scope
, a
) && hasConstantStrictBooleanComparison(scope
, b
))
426 //------------------------------------------------------------------------------
428 //------------------------------------------------------------------------------
430 /** @type {import('../shared/types').Rule} */
435 description
: "Disallow expressions where the operation doesn't affect the value",
437 url
: "https://eslint.org/docs/rules/no-constant-binary-expression"
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."
450 LogicalExpression(node
) {
451 const { operator
, left
} = node
;
452 const scope
= context
.getScope();
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
} });
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
);
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" });
476 } else if (operator
=== "==" || operator
=== "!=") {
479 * If both sides are "new", then both sides are objects and
480 * therefore they will be compared by reference even with `==`
483 if (isAlwaysNew(scope
, left
) && isAlwaysNew(scope
, right
)) {
484 context
.report({ node
: left
, messageId
: "bothAlwaysNew" });
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
496 * AssignmentExpression() {},