Skip to content

Commit 71f6f0d

Browse files
feat: report more cases with ?? in no-constant-binary-expression (#16826)
* feat: update rule for unnecessary nullish coalescing operator * docs: add incorrect cases * fix: update logic by review * Update docs/src/rules/no-constant-binary-expression.md Co-authored-by: Milos Djermanovic <[email protected]> * wip * fix: update logic for false negative * fix; remove unnecessary diff * Update tests/lib/rules/no-constant-binary-expression.js Co-authored-by: Milos Djermanovic <[email protected]> --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent ed2999b commit 71f6f0d

File tree

3 files changed

+47
-26
lines changed

3 files changed

+47
-26
lines changed

docs/src/rules/no-constant-binary-expression.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ const value4 = new Boolean(foo) === true;
5353
const objIsEmpty = someObj === {};
5454

5555
const arrIsEmpty = someArr === [];
56+
57+
const shortCircuit1 = condition1 && false && condition2;
58+
59+
const shortCircuit2 = condition1 || true || condition2;
60+
61+
const shortCircuit3 = condition1 ?? "non-nullish" ?? condition2;
5662
```
5763
5864
:::

lib/rules/no-constant-binary-expression.js

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,38 @@ const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|"
1414
// Helpers
1515
//------------------------------------------------------------------------------
1616

17+
/**
18+
* Checks whether or not a node is `null` or `undefined`. Similar to the one
19+
* found in ast-utils.js, but this one correctly handles the edge case that
20+
* `undefined` has been redefined.
21+
* @param {Scope} scope Scope in which the expression was found.
22+
* @param {ASTNode} node A node to check.
23+
* @returns {boolean} Whether or not the node is a `null` or `undefined`.
24+
* @public
25+
*/
26+
function isNullOrUndefined(scope, node) {
27+
return (
28+
isNullLiteral(node) ||
29+
(node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
30+
(node.type === "UnaryExpression" && node.operator === "void")
31+
);
32+
}
33+
1734
/**
1835
* Test if an AST node has a statically knowable constant nullishness. Meaning,
1936
* it will always resolve to a constant value of either: `null`, `undefined`
2037
* or not `null` _or_ `undefined`. An expression that can vary between those
2138
* three states at runtime would return `false`.
2239
* @param {Scope} scope The scope in which the node was found.
2340
* @param {ASTNode} node The AST node being tested.
41+
* @param {boolean} nonNullish if `true` then nullish values are not considered constant.
2442
* @returns {boolean} Does `node` have constant nullishness?
2543
*/
26-
function hasConstantNullishness(scope, node) {
44+
function hasConstantNullishness(scope, node, nonNullish) {
45+
if (nonNullish && isNullOrUndefined(scope, node)) {
46+
return false;
47+
}
48+
2749
switch (node.type) {
2850
case "ObjectExpression": // Objects are never nullish
2951
case "ArrayExpression": // Arrays are never nullish
@@ -45,9 +67,12 @@ function hasConstantNullishness(scope, node) {
4567
return (functionName === "Boolean" || functionName === "String" || functionName === "Number") &&
4668
isReferenceToGlobalVariable(scope, node.callee);
4769
}
70+
case "LogicalExpression": {
71+
return node.operator === "??" && hasConstantNullishness(scope, node.right, true);
72+
}
4873
case "AssignmentExpression":
4974
if (node.operator === "=") {
50-
return hasConstantNullishness(scope, node.right);
75+
return hasConstantNullishness(scope, node.right, nonNullish);
5176
}
5277

5378
/*
@@ -80,7 +105,7 @@ function hasConstantNullishness(scope, node) {
80105
case "SequenceExpression": {
81106
const last = node.expressions[node.expressions.length - 1];
82107

83-
return hasConstantNullishness(scope, last);
108+
return hasConstantNullishness(scope, last, nonNullish);
84109
}
85110
case "Identifier":
86111
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
@@ -378,24 +403,6 @@ function isAlwaysNew(scope, node) {
378403
}
379404
}
380405

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-
399406
/**
400407
* Checks if one operand will cause the result to be constant.
401408
* @param {Scope} scope Scope in which the expression was found.
@@ -407,14 +414,14 @@ function isNullOrUndefined(scope, node) {
407414
function findBinaryExpressionConstantOperand(scope, a, b, operator) {
408415
if (operator === "==" || operator === "!=") {
409416
if (
410-
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
417+
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) ||
411418
(isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))
412419
) {
413420
return b;
414421
}
415422
} else if (operator === "===" || operator === "!==") {
416423
if (
417-
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
424+
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) ||
418425
(isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))
419426
) {
420427
return b;
@@ -453,7 +460,7 @@ module.exports = {
453460

454461
if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {
455462
context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });
456-
} else if (operator === "??" && hasConstantNullishness(scope, left)) {
463+
} else if (operator === "??" && hasConstantNullishness(scope, left, false)) {
457464
context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });
458465
}
459466
},

tests/lib/rules/no-constant-binary-expression.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ ruleTester.run("no-constant-binary-expression", rule, {
5959
"function foo(undefined) { undefined === true;}",
6060
"[...arr, 1] == true",
6161
"[,,,] == true",
62-
{ code: "new Foo() === bar;", globals: { Foo: "writable" } }
62+
{ code: "new Foo() === bar;", globals: { Foo: "writable" } },
63+
"(foo && true) ?? bar",
64+
"foo ?? null ?? bar",
65+
"a ?? (doSomething(), undefined) ?? b",
66+
"a ?? (something = null) ?? b"
6367
],
6468
invalid: [
6569

@@ -308,6 +312,10 @@ ruleTester.run("no-constant-binary-expression", rule, {
308312
{ code: "x === /[a-z]/", errors: [{ messageId: "alwaysNew" }] },
309313

310314
// It's not obvious what this does, but it compares the old value of `x` to the new object.
311-
{ code: "x === (x = {})", errors: [{ messageId: "alwaysNew" }] }
315+
{ code: "x === (x = {})", errors: [{ messageId: "alwaysNew" }] },
316+
317+
{ code: "window.abc && false && anything", errors: [{ messageId: "constantShortCircuit" }] },
318+
{ code: "window.abc || true || anything", errors: [{ messageId: "constantShortCircuit" }] },
319+
{ code: "window.abc ?? 'non-nullish' ?? anything", errors: [{ messageId: "constantShortCircuit" }] }
312320
]
313321
});

0 commit comments

Comments
 (0)