build: update npm install command to use --legacy-peer-deps
This change modifies the build scripts to use --legacy-peer-deps flag when installing npm dependencies to resolve potential peer dependency conflicts during the build process. The change is applied to both bash (build.sh) and batch (build.bat) build scripts.
This commit is contained in:
+383
-477
@@ -5,28 +5,10 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
isNullLiteral,
|
||||
isConstant,
|
||||
isReferenceToGlobalVariable,
|
||||
isLogicalAssignmentOperator,
|
||||
ECMASCRIPT_GLOBALS,
|
||||
} = require("./utils/ast-utils");
|
||||
const globals = require("globals");
|
||||
const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");
|
||||
|
||||
const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set([
|
||||
"+",
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"%",
|
||||
"|",
|
||||
"^",
|
||||
"&",
|
||||
"**",
|
||||
"<<",
|
||||
">>",
|
||||
">>>",
|
||||
]);
|
||||
const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -42,13 +24,11 @@ const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set([
|
||||
* @public
|
||||
*/
|
||||
function isNullOrUndefined(scope, node) {
|
||||
return (
|
||||
isNullLiteral(node) ||
|
||||
(node.type === "Identifier" &&
|
||||
node.name === "undefined" &&
|
||||
isReferenceToGlobalVariable(scope, node)) ||
|
||||
(node.type === "UnaryExpression" && node.operator === "void")
|
||||
);
|
||||
return (
|
||||
isNullLiteral(node) ||
|
||||
(node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
|
||||
(node.type === "UnaryExpression" && node.operator === "void")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,88 +42,79 @@ function isNullOrUndefined(scope, node) {
|
||||
* @returns {boolean} Does `node` have constant nullishness?
|
||||
*/
|
||||
function hasConstantNullishness(scope, node, nonNullish) {
|
||||
if (nonNullish && isNullOrUndefined(scope, node)) {
|
||||
return false;
|
||||
}
|
||||
if (nonNullish && isNullOrUndefined(scope, node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "ObjectExpression": // Objects are never nullish
|
||||
case "ArrayExpression": // Arrays are never nullish
|
||||
case "ArrowFunctionExpression": // Functions never nullish
|
||||
case "FunctionExpression": // Functions are never nullish
|
||||
case "ClassExpression": // Classes are never nullish
|
||||
case "NewExpression": // Objects are never nullish
|
||||
case "Literal": // Nullish, or non-nullish, literals never change
|
||||
case "TemplateLiteral": // A string is never nullish
|
||||
case "UpdateExpression": // Numbers are never nullish
|
||||
case "BinaryExpression": // Numbers, strings, or booleans are never nullish
|
||||
return true;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
const functionName = node.callee.name;
|
||||
switch (node.type) {
|
||||
case "ObjectExpression": // Objects are never nullish
|
||||
case "ArrayExpression": // Arrays are never nullish
|
||||
case "ArrowFunctionExpression": // Functions never nullish
|
||||
case "FunctionExpression": // Functions are never nullish
|
||||
case "ClassExpression": // Classes are never nullish
|
||||
case "NewExpression": // Objects are never nullish
|
||||
case "Literal": // Nullish, or non-nullish, literals never change
|
||||
case "TemplateLiteral": // A string is never nullish
|
||||
case "UpdateExpression": // Numbers are never nullish
|
||||
case "BinaryExpression": // Numbers, strings, or booleans are never nullish
|
||||
return true;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
const functionName = node.callee.name;
|
||||
|
||||
return (
|
||||
(functionName === "Boolean" ||
|
||||
functionName === "String" ||
|
||||
functionName === "Number") &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
);
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
return (
|
||||
node.operator === "??" &&
|
||||
hasConstantNullishness(scope, node.right, true)
|
||||
);
|
||||
}
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantNullishness(scope, node.right, nonNullish);
|
||||
}
|
||||
return (functionName === "Boolean" || functionName === "String" || functionName === "Number") &&
|
||||
isReferenceToGlobalVariable(scope, node.callee);
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
return node.operator === "??" && hasConstantNullishness(scope, node.right, true);
|
||||
}
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantNullishness(scope, node.right, nonNullish);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...) /
|
||||
*/
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...) /
|
||||
*/
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* The remaining assignment expressions all result in a numeric or
|
||||
* string (non-nullish) value:
|
||||
* "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
|
||||
*/
|
||||
/*
|
||||
* The remaining assignment expressions all result in a numeric or
|
||||
* string (non-nullish) value:
|
||||
* "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
|
||||
*/
|
||||
|
||||
return true;
|
||||
case "UnaryExpression":
|
||||
/*
|
||||
* "void" Always returns `undefined`
|
||||
* "typeof" All types are strings, and thus non-nullish
|
||||
* "!" Boolean is never nullish
|
||||
* "delete" Returns a boolean, which is never nullish
|
||||
* Math operators always return numbers or strings, neither of which
|
||||
* are non-nullish "+", "-", "~"
|
||||
*/
|
||||
return true;
|
||||
case "UnaryExpression":
|
||||
|
||||
return true;
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions.at(-1);
|
||||
/*
|
||||
* "void" Always returns `undefined`
|
||||
* "typeof" All types are strings, and thus non-nullish
|
||||
* "!" Boolean is never nullish
|
||||
* "delete" Returns a boolean, which is never nullish
|
||||
* Math operators always return numbers or strings, neither of which
|
||||
* are non-nullish "+", "-", "~"
|
||||
*/
|
||||
|
||||
return hasConstantNullishness(scope, last, nonNullish);
|
||||
}
|
||||
case "Identifier":
|
||||
return (
|
||||
node.name === "undefined" &&
|
||||
isReferenceToGlobalVariable(scope, node)
|
||||
);
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return hasConstantNullishness(scope, last, nonNullish);
|
||||
}
|
||||
case "Identifier":
|
||||
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,149 +128,139 @@ function hasConstantNullishness(scope, node, nonNullish) {
|
||||
* @returns {boolean} Is `node` guaranteed to be a boolean?
|
||||
*/
|
||||
function isStaticBoolean(scope, node) {
|
||||
switch (node.type) {
|
||||
case "Literal":
|
||||
return typeof node.value === "boolean";
|
||||
case "CallExpression":
|
||||
return (
|
||||
node.callee.type === "Identifier" &&
|
||||
node.callee.name === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee) &&
|
||||
(node.arguments.length === 0 ||
|
||||
isConstant(scope, node.arguments[0], true))
|
||||
);
|
||||
case "UnaryExpression":
|
||||
return (
|
||||
node.operator === "!" && isConstant(scope, node.argument, true)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (node.type) {
|
||||
case "Literal":
|
||||
return typeof node.value === "boolean";
|
||||
case "CallExpression":
|
||||
return node.callee.type === "Identifier" && node.callee.name === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee) &&
|
||||
(node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
|
||||
case "UnaryExpression":
|
||||
return node.operator === "!" && isConstant(scope, node.argument, true);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if an AST node will always give the same result when compared to a
|
||||
* boolean value. Note that comparison to boolean values is different than
|
||||
* truthiness.
|
||||
* https://262.ecma-international.org/5.1/#sec-11.9.3
|
||||
*
|
||||
* JavaScript `==` operator works by converting the boolean to `1` (true) or
|
||||
* Javascript `==` operator works by converting the boolean to `1` (true) or
|
||||
* `+0` (false) and then checks the values `==` equality to that number.
|
||||
* @param {Scope} scope The scope in which node was found.
|
||||
* @param {ASTNode} node The node to test.
|
||||
* @returns {boolean} Will `node` always coerce to the same boolean value?
|
||||
*/
|
||||
function hasConstantLooseBooleanComparison(scope, node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ClassExpression":
|
||||
/**
|
||||
* 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 "ArrayExpression": {
|
||||
const nonSpreadElements = node.elements.filter(
|
||||
e =>
|
||||
// Elements can be `null` in sparse arrays: `[,,]`;
|
||||
e !== null && e.type !== "SpreadElement",
|
||||
);
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ClassExpression":
|
||||
|
||||
/*
|
||||
* Possible future direction if needed: We could check if the
|
||||
* single value would result in variable boolean comparison.
|
||||
* For now we will err on the side of caution since `[x]` could
|
||||
* evaluate to `[0]` or `[1]`.
|
||||
*/
|
||||
return node.elements.length === 0 || nonSpreadElements.length > 1;
|
||||
}
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
return true;
|
||||
case "UnaryExpression":
|
||||
if (
|
||||
node.operator === "void" || // Always returns `undefined`
|
||||
node.operator === "typeof" // All `typeof` strings, when coerced to number, are not 0 or 1.
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (node.operator === "!") {
|
||||
return isConstant(scope, node.argument, true);
|
||||
}
|
||||
/**
|
||||
* 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 "ArrayExpression": {
|
||||
const nonSpreadElements = node.elements.filter(e =>
|
||||
|
||||
/*
|
||||
* We won't try to reason about +, -, ~, or delete
|
||||
* In theory, for the mathematical operators, we could look at the
|
||||
* argument and try to determine if it coerces to a constant numeric
|
||||
* value.
|
||||
*/
|
||||
return false;
|
||||
case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.
|
||||
return false;
|
||||
case "CallExpression": {
|
||||
if (
|
||||
node.callee.type === "Identifier" &&
|
||||
node.callee.name === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return (
|
||||
node.arguments.length === 0 ||
|
||||
isConstant(scope, node.arguments[0], true)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "Literal": // True or false, literals never change
|
||||
return true;
|
||||
case "Identifier":
|
||||
return (
|
||||
node.name === "undefined" &&
|
||||
isReferenceToGlobalVariable(scope, node)
|
||||
);
|
||||
case "TemplateLiteral":
|
||||
/*
|
||||
* In theory we could try to check if the quasi are sufficient to
|
||||
* prove that the expression will always be true, but it would be
|
||||
* tricky to get right. For example: `000.${foo}000`
|
||||
*/
|
||||
return node.expressions.length === 0;
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantLooseBooleanComparison(scope, node.right);
|
||||
}
|
||||
// Elements can be `null` in sparse arrays: `[,,]`;
|
||||
e !== null && e.type !== "SpreadElement");
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...)
|
||||
*
|
||||
* The remaining assignment expressions all result in a numeric or
|
||||
* string (non-nullish) values which could be truthy or falsy:
|
||||
* "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
|
||||
*/
|
||||
return false;
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions.at(-1);
|
||||
|
||||
return hasConstantLooseBooleanComparison(scope, last);
|
||||
}
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
* Possible future direction if needed: We could check if the
|
||||
* single value would result in variable boolean comparison.
|
||||
* For now we will err on the side of caution since `[x]` could
|
||||
* evaluate to `[0]` or `[1]`.
|
||||
*/
|
||||
return node.elements.length === 0 || nonSpreadElements.length > 1;
|
||||
}
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
return true;
|
||||
case "UnaryExpression":
|
||||
if (node.operator === "void" || // Always returns `undefined`
|
||||
node.operator === "typeof" // All `typeof` strings, when coerced to number, are not 0 or 1.
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (node.operator === "!") {
|
||||
return isConstant(scope, node.argument, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* We won't try to reason about +, -, ~, or delete
|
||||
* In theory, for the mathematical operators, we could look at the
|
||||
* argument and try to determine if it coerces to a constant numeric
|
||||
* value.
|
||||
*/
|
||||
return false;
|
||||
case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.
|
||||
return false;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type === "Identifier" &&
|
||||
node.callee.name === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return node.arguments.length === 0 || isConstant(scope, node.arguments[0], true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "Literal": // True or false, literals never change
|
||||
return true;
|
||||
case "Identifier":
|
||||
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
|
||||
case "TemplateLiteral":
|
||||
|
||||
/*
|
||||
* In theory we could try to check if the quasi are sufficient to
|
||||
* prove that the expression will always be true, but it would be
|
||||
* tricky to get right. For example: `000.${foo}000`
|
||||
*/
|
||||
return node.expressions.length === 0;
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantLooseBooleanComparison(scope, node.right);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...)
|
||||
*
|
||||
* The remaining assignment expressions all result in a numeric or
|
||||
* string (non-nullish) values which could be truthy or falsy:
|
||||
* "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
|
||||
*/
|
||||
return false;
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return hasConstantLooseBooleanComparison(scope, last);
|
||||
}
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if an AST node will always give the same result when _strictly_ compared
|
||||
* to a boolean value. This can happen if the expression can never be boolean, or
|
||||
@@ -310,90 +271,82 @@ function hasConstantLooseBooleanComparison(scope, node) {
|
||||
* static boolean value?
|
||||
*/
|
||||
function hasConstantStrictBooleanComparison(scope, node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression": // Objects are not booleans
|
||||
case "ArrayExpression": // Arrays are not booleans
|
||||
case "ArrowFunctionExpression": // Functions are not booleans
|
||||
case "FunctionExpression":
|
||||
case "ClassExpression": // Classes are not booleans
|
||||
case "NewExpression": // Objects are not booleans
|
||||
case "TemplateLiteral": // Strings are not booleans
|
||||
case "Literal": // True, false, or not boolean, literals never change.
|
||||
case "UpdateExpression": // Numbers are not booleans
|
||||
return true;
|
||||
case "BinaryExpression":
|
||||
return NUMERIC_OR_STRING_BINARY_OPERATORS.has(node.operator);
|
||||
case "UnaryExpression": {
|
||||
if (node.operator === "delete") {
|
||||
return false;
|
||||
}
|
||||
if (node.operator === "!") {
|
||||
return isConstant(scope, node.argument, true);
|
||||
}
|
||||
switch (node.type) {
|
||||
case "ObjectExpression": // Objects are not booleans
|
||||
case "ArrayExpression": // Arrays are not booleans
|
||||
case "ArrowFunctionExpression": // Functions are not booleans
|
||||
case "FunctionExpression":
|
||||
case "ClassExpression": // Classes are not booleans
|
||||
case "NewExpression": // Objects are not booleans
|
||||
case "TemplateLiteral": // Strings are not booleans
|
||||
case "Literal": // True, false, or not boolean, literals never change.
|
||||
case "UpdateExpression": // Numbers are not booleans
|
||||
return true;
|
||||
case "BinaryExpression":
|
||||
return NUMERIC_OR_STRING_BINARY_OPERATORS.has(node.operator);
|
||||
case "UnaryExpression": {
|
||||
if (node.operator === "delete") {
|
||||
return false;
|
||||
}
|
||||
if (node.operator === "!") {
|
||||
return isConstant(scope, node.argument, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* The remaining operators return either strings or numbers, neither
|
||||
* of which are boolean.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions.at(-1);
|
||||
/*
|
||||
* The remaining operators return either strings or numbers, neither
|
||||
* of which are boolean.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return hasConstantStrictBooleanComparison(scope, last);
|
||||
}
|
||||
case "Identifier":
|
||||
return (
|
||||
node.name === "undefined" &&
|
||||
isReferenceToGlobalVariable(scope, node)
|
||||
);
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantStrictBooleanComparison(scope, node.right);
|
||||
}
|
||||
return hasConstantStrictBooleanComparison(scope, last);
|
||||
}
|
||||
case "Identifier":
|
||||
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantStrictBooleanComparison(scope, node.right);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...)
|
||||
*/
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...)
|
||||
*/
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* The remaining assignment expressions all result in either a number
|
||||
* or a string, neither of which can ever be boolean.
|
||||
*/
|
||||
return true;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
const functionName = node.callee.name;
|
||||
/*
|
||||
* The remaining assignment expressions all result in either a number
|
||||
* or a string, neither of which can ever be boolean.
|
||||
*/
|
||||
return true;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
const functionName = node.callee.name;
|
||||
|
||||
if (
|
||||
(functionName === "String" || functionName === "Number") &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
functionName === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return (
|
||||
node.arguments.length === 0 ||
|
||||
isConstant(scope, node.arguments[0], true)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(functionName === "String" || functionName === "Number") &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (functionName === "Boolean" && isReferenceToGlobalVariable(scope, node.callee)) {
|
||||
return (
|
||||
node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,55 +356,51 @@ function hasConstantStrictBooleanComparison(scope, node) {
|
||||
* @returns {boolean} Will `node` always be new?
|
||||
*/
|
||||
function isAlwaysNew(scope, node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ArrayExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
case "ClassExpression":
|
||||
return true;
|
||||
case "NewExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ArrayExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
case "ClassExpression":
|
||||
return true;
|
||||
case "NewExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* All the built-in constructors are always new, but
|
||||
* user-defined constructors could return a sentinel
|
||||
* object.
|
||||
*
|
||||
* Catching these is especially useful for primitive constructors
|
||||
* which return boxed values, a surprising gotcha' in JavaScript.
|
||||
*/
|
||||
return (
|
||||
Object.hasOwn(ECMASCRIPT_GLOBALS, node.callee.name) &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
);
|
||||
}
|
||||
case "Literal":
|
||||
// Regular expressions are objects, and thus always new
|
||||
return typeof node.regex === "object";
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions.at(-1);
|
||||
/*
|
||||
* All the built-in constructors are always new, but
|
||||
* user-defined constructors could return a sentinel
|
||||
* object.
|
||||
*
|
||||
* Catching these is especially useful for primitive constructors
|
||||
* which return boxed values, a surprising gotcha' in JavaScript.
|
||||
*/
|
||||
return Object.hasOwnProperty.call(globals.builtin, node.callee.name) &&
|
||||
isReferenceToGlobalVariable(scope, node.callee);
|
||||
}
|
||||
case "Literal":
|
||||
|
||||
return isAlwaysNew(scope, last);
|
||||
}
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return isAlwaysNew(scope, node.right);
|
||||
}
|
||||
return false;
|
||||
case "ConditionalExpression":
|
||||
return (
|
||||
isAlwaysNew(scope, node.consequent) &&
|
||||
isAlwaysNew(scope, node.alternate)
|
||||
);
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
// Regular expressions are objects, and thus always new
|
||||
return typeof node.regex === "object";
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return isAlwaysNew(scope, last);
|
||||
}
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return isAlwaysNew(scope, node.right);
|
||||
}
|
||||
return false;
|
||||
case "ConditionalExpression":
|
||||
return isAlwaysNew(scope, node.consequent) && isAlwaysNew(scope, node.alternate);
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -463,141 +412,98 @@ function isAlwaysNew(scope, node) {
|
||||
* @returns {ASTNode | null} The node which will cause the expression to have a constant result.
|
||||
*/
|
||||
function findBinaryExpressionConstantOperand(scope, a, b, operator) {
|
||||
if (operator === "==" || operator === "!=") {
|
||||
if (
|
||||
(isNullOrUndefined(scope, a) &&
|
||||
hasConstantNullishness(scope, b, false)) ||
|
||||
(isStaticBoolean(scope, a) &&
|
||||
hasConstantLooseBooleanComparison(scope, b))
|
||||
) {
|
||||
return b;
|
||||
}
|
||||
} else if (operator === "===" || operator === "!==") {
|
||||
if (
|
||||
(isNullOrUndefined(scope, a) &&
|
||||
hasConstantNullishness(scope, b, false)) ||
|
||||
(isStaticBoolean(scope, a) &&
|
||||
hasConstantStrictBooleanComparison(scope, b))
|
||||
) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
if (operator === "==" || operator === "!=") {
|
||||
if (
|
||||
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) ||
|
||||
(isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))
|
||||
) {
|
||||
return b;
|
||||
}
|
||||
} else if (operator === "===" || operator === "!==") {
|
||||
if (
|
||||
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) ||
|
||||
(isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))
|
||||
) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../types').Rule.RuleModule} */
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow expressions where the operation doesn't affect the value",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-constant-binary-expression",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
constantBinaryOperand:
|
||||
"Unexpected constant binary expression. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.",
|
||||
constantShortCircuit:
|
||||
"Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression.",
|
||||
alwaysNew:
|
||||
"Unexpected comparison to newly constructed object. These two values can never be equal.",
|
||||
bothAlwaysNew:
|
||||
"Unexpected comparison of two newly constructed objects. These two values can never be equal.",
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow expressions where the operation doesn't affect the value",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-constant-binary-expression"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
constantBinaryOperand: "Unexpected constant binary expression. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.",
|
||||
constantShortCircuit: "Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression.",
|
||||
alwaysNew: "Unexpected comparison to newly constructed object. These two values can never be equal.",
|
||||
bothAlwaysNew: "Unexpected comparison of two newly constructed objects. These two values can never be equal."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
LogicalExpression(node) {
|
||||
const { operator, left } = node;
|
||||
const scope = sourceCode.getScope(node);
|
||||
return {
|
||||
LogicalExpression(node) {
|
||||
const { operator, left } = node;
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
if (
|
||||
(operator === "&&" || operator === "||") &&
|
||||
isConstant(scope, left, true)
|
||||
) {
|
||||
context.report({
|
||||
node: left,
|
||||
messageId: "constantShortCircuit",
|
||||
data: { property: "truthiness", operator },
|
||||
});
|
||||
} else if (
|
||||
operator === "??" &&
|
||||
hasConstantNullishness(scope, left, false)
|
||||
) {
|
||||
context.report({
|
||||
node: left,
|
||||
messageId: "constantShortCircuit",
|
||||
data: { property: "nullishness", operator },
|
||||
});
|
||||
}
|
||||
},
|
||||
BinaryExpression(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const { right, left, operator } = node;
|
||||
const rightConstantOperand =
|
||||
findBinaryExpressionConstantOperand(
|
||||
scope,
|
||||
left,
|
||||
right,
|
||||
operator,
|
||||
);
|
||||
const leftConstantOperand = findBinaryExpressionConstantOperand(
|
||||
scope,
|
||||
right,
|
||||
left,
|
||||
operator,
|
||||
);
|
||||
if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {
|
||||
context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });
|
||||
} else if (operator === "??" && hasConstantNullishness(scope, left, false)) {
|
||||
context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });
|
||||
}
|
||||
},
|
||||
BinaryExpression(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const { right, left, operator } = node;
|
||||
const rightConstantOperand = findBinaryExpressionConstantOperand(scope, left, right, operator);
|
||||
const leftConstantOperand = findBinaryExpressionConstantOperand(scope, right, left, operator);
|
||||
|
||||
if (rightConstantOperand) {
|
||||
context.report({
|
||||
node: rightConstantOperand,
|
||||
messageId: "constantBinaryOperand",
|
||||
data: { operator, otherSide: "left" },
|
||||
});
|
||||
} else if (leftConstantOperand) {
|
||||
context.report({
|
||||
node: leftConstantOperand,
|
||||
messageId: "constantBinaryOperand",
|
||||
data: { operator, otherSide: "right" },
|
||||
});
|
||||
} else if (operator === "===" || operator === "!==") {
|
||||
if (isAlwaysNew(scope, left)) {
|
||||
context.report({ node: left, messageId: "alwaysNew" });
|
||||
} else if (isAlwaysNew(scope, right)) {
|
||||
context.report({ node: right, messageId: "alwaysNew" });
|
||||
}
|
||||
} else if (operator === "==" || operator === "!=") {
|
||||
/*
|
||||
* If both sides are "new", then both sides are objects and
|
||||
* therefore they will be compared by reference even with `==`
|
||||
* equality.
|
||||
*/
|
||||
if (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {
|
||||
context.report({
|
||||
node: left,
|
||||
messageId: "bothAlwaysNew",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
if (rightConstantOperand) {
|
||||
context.report({ node: rightConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "left" } });
|
||||
} else if (leftConstantOperand) {
|
||||
context.report({ node: leftConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "right" } });
|
||||
} else if (operator === "===" || operator === "!==") {
|
||||
if (isAlwaysNew(scope, left)) {
|
||||
context.report({ node: left, messageId: "alwaysNew" });
|
||||
} else if (isAlwaysNew(scope, right)) {
|
||||
context.report({ node: right, messageId: "alwaysNew" });
|
||||
}
|
||||
} else if (operator === "==" || operator === "!=") {
|
||||
|
||||
/*
|
||||
* In theory we could handle short-circuiting assignment operators,
|
||||
* for some constant values, but that would require walking the
|
||||
* scope to find the value of the variable being assigned. This is
|
||||
* dependent on https://github.com/eslint/eslint/issues/13776
|
||||
*
|
||||
* AssignmentExpression() {},
|
||||
*/
|
||||
};
|
||||
},
|
||||
/*
|
||||
* If both sides are "new", then both sides are objects and
|
||||
* therefore they will be compared by reference even with `==`
|
||||
* equality.
|
||||
*/
|
||||
if (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {
|
||||
context.report({ node: left, messageId: "bothAlwaysNew" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* In theory we could handle short-circuiting assignment operators,
|
||||
* for some constant values, but that would require walking the
|
||||
* scope to find the value of the variable being assigned. This is
|
||||
* dependant on https://github.com/eslint/eslint/issues/13776
|
||||
*
|
||||
* AssignmentExpression() {},
|
||||
*/
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user