Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'tools/node_modules/eslint/lib/rules/no-constant-binary-expression.js')
-rw-r--r--tools/node_modules/eslint/lib/rules/no-constant-binary-expression.js500
1 files changed, 500 insertions, 0 deletions
diff --git a/tools/node_modules/eslint/lib/rules/no-constant-binary-expression.js b/tools/node_modules/eslint/lib/rules/no-constant-binary-expression.js
new file mode 100644
index 00000000000..d550bcf1d91
--- /dev/null
+++ b/tools/node_modules/eslint/lib/rules/no-constant-binary-expression.js
@@ -0,0 +1,500 @@
+/**
+ * @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit
+ * @author Jordan Eldredge <https://jordaneldredge.com>
+ */
+
+"use strict";
+
+const globals = require("globals");
+const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");
+
+const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Test if an AST node has a statically knowable constant nullishness. Meaning,
+ * it will always resolve to a constant value of either: `null`, `undefined`
+ * or not `null` _or_ `undefined`. An expression that can vary between those
+ * three states at runtime would return `false`.
+ * @param {Scope} scope The scope in which the node was found.
+ * @param {ASTNode} node The AST node being tested.
+ * @returns {boolean} Does `node` have constant nullishness?
+ */
+function hasConstantNullishness(scope, node) {
+ 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 "AssignmentExpression":
+ if (node.operator === "=") {
+ return hasConstantNullishness(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;
+ }
+
+ /*
+ * 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 "SequenceExpression": {
+ const last = node.expressions[node.expressions.length - 1];
+
+ return hasConstantNullishness(scope, last);
+ }
+ 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;
+ }
+}
+
+/**
+ * Test if an AST node is a boolean value that never changes. Specifically we
+ * test for:
+ * 1. Literal booleans (`true` or `false`)
+ * 2. Unary `!` expressions with a constant value
+ * 3. Constant booleans created via the `Boolean` global function
+ * @param {Scope} scope The scope in which the node was found.
+ * @param {ASTNode} node The node to test
+ * @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;
+ }
+}
+
+
+/**
+ * Test if an AST node will always give the same result when compared to a
+ * bolean 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
+ * `+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");
+
+
+ /*
+ * 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 bolean value. This can happen if the expression can never be boolean, or
+ * if it is always the same boolean value.
+ * @param {Scope} scope The scope in which the node was found.
+ * @param {ASTNode} node The node to test
+ * @returns {boolean} Will `node` always give the same result when compared to a
+ * 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);
+ }
+
+ /*
+ * 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);
+ }
+
+ /*
+ * 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;
+
+ 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;
+ }
+}
+
+/**
+ * Test if an AST node will always result in a newly constructed object
+ * @param {Scope} scope The scope in which the node was found.
+ * @param {ASTNode} node The node to test
+ * @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;
+ }
+
+ /*
+ * All the built-in constructors are always new, but
+ * user-defined constructors could return a sentinel
+ * object.
+ *
+ * Catching these is especially useful for primitive constructures
+ * which return boxed values, a surprising gotcha' in JavaScript.
+ */
+ return Object.hasOwnProperty.call(globals.builtin, 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[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;
+ }
+}
+
+/**
+ * Checks whether or not a node is `null` or `undefined`. Similar to the one
+ * found in ast-utils.js, but this one correctly handles the edge case that
+ * `undefined` has been redefined.
+ * @param {Scope} scope Scope in which the expression was found.
+ * @param {ASTNode} node A node to check.
+ * @returns {boolean} Whether or not the node is a `null` or `undefined`.
+ * @public
+ */
+function isNullOrUndefined(scope, node) {
+ return (
+ isNullLiteral(node) ||
+ (node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
+ (node.type === "UnaryExpression" && node.operator === "void")
+ );
+}
+
+
+/**
+ * Checks if one operand will cause the result to be constant.
+ * @param {Scope} scope Scope in which the expression was found.
+ * @param {ASTNode} a One side of the expression
+ * @param {ASTNode} b The other side of the expression
+ * @param {string} operator The binary expression operator
+ * @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)) ||
+ (isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))
+ ) {
+ return b;
+ }
+ } else if (operator === "===" || operator === "!==") {
+ if (
+ (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
+ (isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))
+ ) {
+ return b;
+ }
+ }
+ return null;
+}
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+/** @type {import('../shared/types').Rule} */
+module.exports = {
+ meta: {
+ type: "problem",
+ docs: {
+ description: "disallow expressions where the operation doesn't affect the value",
+ recommended: false,
+ url: "https://eslint.org/docs/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) {
+ return {
+ LogicalExpression(node) {
+ const { operator, left } = node;
+ const scope = context.getScope();
+
+ if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {
+ context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });
+ } else if (operator === "??" && hasConstantNullishness(scope, left)) {
+ context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });
+ }
+ },
+ BinaryExpression(node) {
+ const scope = context.getScope();
+ 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" });
+ }
+ }
+
+ }
+
+ /*
+ * In theory we could handle short circuting 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() {},
+ */
+ };
+ }
+};