blob: 8adc9bca4db67d69e4f65c878bd6d462ed02f8a4 [file] [log] [blame]
/**
* @fileoverview Rule to flag use constant conditions
* @author Christian Schulz <http://rndm.de>
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "disallow constant expressions in conditions",
recommended: true,
url: "https://eslint.org/docs/rules/no-constant-condition"
},
schema: [
{
type: "object",
properties: {
checkLoops: {
type: "boolean",
default: true
}
},
additionalProperties: false
}
],
messages: {
unexpected: "Unexpected constant condition."
}
},
create(context) {
const options = context.options[0] || {},
checkLoops = options.checkLoops !== false,
loopSetStack = [];
let loopsInCurrentScope = new Set();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Returns literal's value converted to the Boolean type
* @param {ASTNode} node any `Literal` node
* @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
* `null` when it cannot be determined.
*/
function getBooleanValue(node) {
if (node.value === null) {
/*
* it might be a null literal or bigint/regex literal in unsupported environments .
* https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
* https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
*/
if (node.raw === "null") {
return false;
}
// regex is always truthy
if (typeof node.regex === "object") {
return true;
}
return null;
}
return !!node.value;
}
/**
* Checks if a branch node of LogicalExpression short circuits the whole condition
* @param {ASTNode} node The branch of main condition which needs to be checked
* @param {string} operator The operator of the main LogicalExpression.
* @returns {boolean} true when condition short circuits whole condition
*/
function isLogicalIdentity(node, operator) {
switch (node.type) {
case "Literal":
return (operator === "||" && getBooleanValue(node) === true) ||
(operator === "&&" && getBooleanValue(node) === false);
case "UnaryExpression":
return (operator === "&&" && node.operator === "void");
case "LogicalExpression":
/*
* handles `a && false || b`
* `false` is an identity element of `&&` but not `||`
*/
return operator === node.operator &&
(
isLogicalIdentity(node.left, operator) ||
isLogicalIdentity(node.right, operator)
);
case "AssignmentExpression":
return ["||=", "&&="].includes(node.operator) &&
operator === node.operator.slice(0, -1) &&
isLogicalIdentity(node.right, operator);
// no default
}
return false;
}
/**
* Checks if a node has a constant truthiness value.
* @param {ASTNode} node The AST node to check.
* @param {boolean} inBooleanPosition `false` if checking branch of a condition.
* `true` in all other cases. When `false`, checks if -- for both string and
* number -- if coerced to that type, the value will be constant.
* @returns {Bool} true when node's truthiness is constant
* @private
*/
function isConstant(node, inBooleanPosition) {
// node.elements can return null values in the case of sparse arrays ex. [,]
if (!node) {
return true;
}
switch (node.type) {
case "Literal":
case "ArrowFunctionExpression":
case "FunctionExpression":
return true;
case "ClassExpression":
case "ObjectExpression":
/**
* In theory objects like:
*
* `{toString: () => a}`
* `{valueOf: () => a}`
*
* Or a classes like:
*
* `class { static toString() { return a } }`
* `class { static valueOf() { return a } }`
*
* Are not constant verifiably when `inBooleanPosition` is
* false, but it's an edge case we've opted not to handle.
*/
return true;
case "TemplateLiteral":
return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
node.expressions.every(exp => isConstant(exp, false));
case "ArrayExpression": {
if (!inBooleanPosition) {
return node.elements.every(element => isConstant(element, false));
}
return true;
}
case "UnaryExpression":
if (
node.operator === "void" ||
node.operator === "typeof" && inBooleanPosition
) {
return true;
}
if (node.operator === "!") {
return isConstant(node.argument, true);
}
return isConstant(node.argument, false);
case "BinaryExpression":
return isConstant(node.left, false) &&
isConstant(node.right, false) &&
node.operator !== "in";
case "LogicalExpression": {
const isLeftConstant = isConstant(node.left, inBooleanPosition);
const isRightConstant = isConstant(node.right, inBooleanPosition);
const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
return (isLeftConstant && isRightConstant) ||
isLeftShortCircuit ||
isRightShortCircuit;
}
case "NewExpression":
return inBooleanPosition;
case "AssignmentExpression":
if (node.operator === "=") {
return isConstant(node.right, inBooleanPosition);
}
if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
return isLogicalIdentity(node.right, node.operator.slice(0, -1));
}
return false;
case "SequenceExpression":
return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
case "SpreadElement":
return isConstant(node.argument, inBooleanPosition);
// no default
}
return false;
}
/**
* Tracks when the given node contains a constant condition.
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function trackConstantConditionLoop(node) {
if (node.test && isConstant(node.test, true)) {
loopsInCurrentScope.add(node);
}
}
/**
* Reports when the set contains the given constant condition node
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function checkConstantConditionLoopInSet(node) {
if (loopsInCurrentScope.has(node)) {
loopsInCurrentScope.delete(node);
context.report({ node: node.test, messageId: "unexpected" });
}
}
/**
* Reports when the given node contains a constant condition.
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function reportIfConstant(node) {
if (node.test && isConstant(node.test, true)) {
context.report({ node: node.test, messageId: "unexpected" });
}
}
/**
* Stores current set of constant loops in loopSetStack temporarily
* and uses a new set to track constant loops
* @returns {void}
* @private
*/
function enterFunction() {
loopSetStack.push(loopsInCurrentScope);
loopsInCurrentScope = new Set();
}
/**
* Reports when the set still contains stored constant conditions
* @returns {void}
* @private
*/
function exitFunction() {
loopsInCurrentScope = loopSetStack.pop();
}
/**
* Checks node when checkLoops option is enabled
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function checkLoop(node) {
if (checkLoops) {
trackConstantConditionLoop(node);
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ConditionalExpression: reportIfConstant,
IfStatement: reportIfConstant,
WhileStatement: checkLoop,
"WhileStatement:exit": checkConstantConditionLoopInSet,
DoWhileStatement: checkLoop,
"DoWhileStatement:exit": checkConstantConditionLoopInSet,
ForStatement: checkLoop,
"ForStatement > .test": node => checkLoop(node.parent),
"ForStatement:exit": checkConstantConditionLoopInSet,
FunctionDeclaration: enterFunction,
"FunctionDeclaration:exit": exitFunction,
FunctionExpression: enterFunction,
"FunctionExpression:exit": exitFunction,
YieldExpression: () => loopsInCurrentScope.clear()
};
}
};