]> git.proxmox.com Git - pve-eslint.git/blobdiff - eslint/lib/rules/utils/ast-utils.js
import 7.12.1 upstream release
[pve-eslint.git] / eslint / lib / rules / utils / ast-utils.js
index ecea6948da236040a023745bd16eee77f9153f76..1fd6340df7c7e46d06c15ea2e8b3215aae5ff86c 100644 (file)
@@ -37,8 +37,12 @@ const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
 // A set of node types that can contain a list of statements
 const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase"]);
 
-const DECIMAL_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u;
-const OCTAL_ESCAPE_PATTERN = /^(?:[^\\]|\\[^0-7]|\\0(?![0-9]))*\\(?:[1-7]|0[0-9])/u;
+const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u;
+
+// Tests the presence of at least one LegacyOctalEscapeSequence or NonOctalDecimalEscapeSequence in a raw string
+const OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN = /^(?:[^\\]|\\.)*\\(?:[1-9]|0[0-9])/su;
+
+const LOGICAL_ASSIGNMENT_OPERATORS = new Set(["&&=", "||=", "??="]);
 
 /**
  * Checks reference if is non initializer and writable.
@@ -143,6 +147,23 @@ function isInLoop(node) {
     return false;
 }
 
+/**
+ * Determines whether the given node is a `null` literal.
+ * @param {ASTNode} node The node to check
+ * @returns {boolean} `true` if the node is a `null` literal
+ */
+function isNullLiteral(node) {
+
+    /*
+     * Checking `node.value === null` does not guarantee that a literal is a null literal.
+     * When parsing values that cannot be represented in the current environment (e.g. unicode
+     * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to
+     * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check
+     * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020
+     */
+    return node.type === "Literal" && node.value === null && !node.regex && !node.bigint;
+}
+
 /**
  * Checks whether or not a node is `null` or `undefined`.
  * @param {ASTNode} node A node to check.
@@ -151,7 +172,7 @@ function isInLoop(node) {
  */
 function isNullOrUndefined(node) {
     return (
-        module.exports.isNullLiteral(node) ||
+        isNullLiteral(node) ||
         (node.type === "Identifier" && node.name === "undefined") ||
         (node.type === "UnaryExpression" && node.operator === "void")
     );
@@ -166,20 +187,270 @@ function isCallee(node) {
     return node.parent.type === "CallExpression" && node.parent.callee === node;
 }
 
+/**
+ * Returns the result of the string conversion applied to the evaluated value of the given expression node,
+ * if it can be determined statically.
+ *
+ * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only.
+ * In all other cases, this function returns `null`.
+ * @param {ASTNode} node Expression node.
+ * @returns {string|null} String value if it can be determined. Otherwise, `null`.
+ */
+function getStaticStringValue(node) {
+    switch (node.type) {
+        case "Literal":
+            if (node.value === null) {
+                if (isNullLiteral(node)) {
+                    return String(node.value); // "null"
+                }
+                if (node.regex) {
+                    return `/${node.regex.pattern}/${node.regex.flags}`;
+                }
+                if (node.bigint) {
+                    return node.bigint;
+                }
+
+                // Otherwise, this is an unknown literal. The function will return null.
+
+            } else {
+                return String(node.value);
+            }
+            break;
+        case "TemplateLiteral":
+            if (node.expressions.length === 0 && node.quasis.length === 1) {
+                return node.quasis[0].value.cooked;
+            }
+            break;
+
+            // no default
+    }
+
+    return null;
+}
+
+/**
+ * Gets the property name of a given node.
+ * The node can be a MemberExpression, a Property, or a MethodDefinition.
+ *
+ * If the name is dynamic, this returns `null`.
+ *
+ * For examples:
+ *
+ *     a.b           // => "b"
+ *     a["b"]        // => "b"
+ *     a['b']        // => "b"
+ *     a[`b`]        // => "b"
+ *     a[100]        // => "100"
+ *     a[b]          // => null
+ *     a["a" + "b"]  // => null
+ *     a[tag`b`]     // => null
+ *     a[`${b}`]     // => null
+ *
+ *     let a = {b: 1}            // => "b"
+ *     let a = {["b"]: 1}        // => "b"
+ *     let a = {['b']: 1}        // => "b"
+ *     let a = {[`b`]: 1}        // => "b"
+ *     let a = {[100]: 1}        // => "100"
+ *     let a = {[b]: 1}          // => null
+ *     let a = {["a" + "b"]: 1}  // => null
+ *     let a = {[tag`b`]: 1}     // => null
+ *     let a = {[`${b}`]: 1}     // => null
+ * @param {ASTNode} node The node to get.
+ * @returns {string|null} The property name if static. Otherwise, null.
+ */
+function getStaticPropertyName(node) {
+    let prop;
+
+    switch (node && node.type) {
+        case "ChainExpression":
+            return getStaticPropertyName(node.expression);
+
+        case "Property":
+        case "MethodDefinition":
+            prop = node.key;
+            break;
+
+        case "MemberExpression":
+            prop = node.property;
+            break;
+
+            // no default
+    }
+
+    if (prop) {
+        if (prop.type === "Identifier" && !node.computed) {
+            return prop.name;
+        }
+
+        return getStaticStringValue(prop);
+    }
+
+    return null;
+}
+
+/**
+ * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
+ * @param {ASTNode} node The node to address.
+ * @returns {ASTNode} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node.
+ */
+function skipChainExpression(node) {
+    return node && node.type === "ChainExpression" ? node.expression : node;
+}
+
+/**
+ * Check if the `actual` is an expected value.
+ * @param {string} actual The string value to check.
+ * @param {string | RegExp} expected The expected string value or pattern.
+ * @returns {boolean} `true` if the `actual` is an expected value.
+ */
+function checkText(actual, expected) {
+    return typeof expected === "string"
+        ? actual === expected
+        : expected.test(actual);
+}
+
+/**
+ * Check if a given node is an Identifier node with a given name.
+ * @param {ASTNode} node The node to check.
+ * @param {string | RegExp} name The expected name or the expected pattern of the object name.
+ * @returns {boolean} `true` if the node is an Identifier node with the name.
+ */
+function isSpecificId(node, name) {
+    return node.type === "Identifier" && checkText(node.name, name);
+}
+
+/**
+ * Check if a given node is member access with a given object name and property name pair.
+ * This is regardless of optional or not.
+ * @param {ASTNode} node The node to check.
+ * @param {string | RegExp | null} objectName The expected name or the expected pattern of the object name. If this is nullish, this method doesn't check object.
+ * @param {string | RegExp | null} propertyName The expected name or the expected pattern of the property name. If this is nullish, this method doesn't check property.
+ * @returns {boolean} `true` if the node is member access with the object name and property name pair.
+ * The node is a `MemberExpression` or `ChainExpression`.
+ */
+function isSpecificMemberAccess(node, objectName, propertyName) {
+    const checkNode = skipChainExpression(node);
+
+    if (checkNode.type !== "MemberExpression") {
+        return false;
+    }
+
+    if (objectName && !isSpecificId(checkNode.object, objectName)) {
+        return false;
+    }
+
+    if (propertyName) {
+        const actualPropertyName = getStaticPropertyName(checkNode);
+
+        if (typeof actualPropertyName !== "string" || !checkText(actualPropertyName, propertyName)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+/**
+ * Check if two literal nodes are the same value.
+ * @param {ASTNode} left The Literal node to compare.
+ * @param {ASTNode} right The other Literal node to compare.
+ * @returns {boolean} `true` if the two literal nodes are the same value.
+ */
+function equalLiteralValue(left, right) {
+
+    // RegExp literal.
+    if (left.regex || right.regex) {
+        return Boolean(
+            left.regex &&
+            right.regex &&
+            left.regex.pattern === right.regex.pattern &&
+            left.regex.flags === right.regex.flags
+        );
+    }
+
+    // BigInt literal.
+    if (left.bigint || right.bigint) {
+        return left.bigint === right.bigint;
+    }
+
+    return left.value === right.value;
+}
+
+/**
+ * Check if two expressions reference the same value. For example:
+ *     a = a
+ *     a.b = a.b
+ *     a[0] = a[0]
+ *     a['b'] = a['b']
+ * @param {ASTNode} left The left side of the comparison.
+ * @param {ASTNode} right The right side of the comparison.
+ * @param {boolean} [disableStaticComputedKey] Don't address `a.b` and `a["b"]` are the same if `true`. For backward compatibility.
+ * @returns {boolean} `true` if both sides match and reference the same value.
+ */
+function isSameReference(left, right, disableStaticComputedKey = false) {
+    if (left.type !== right.type) {
+
+        // Handle `a.b` and `a?.b` are samely.
+        if (left.type === "ChainExpression") {
+            return isSameReference(left.expression, right, disableStaticComputedKey);
+        }
+        if (right.type === "ChainExpression") {
+            return isSameReference(left, right.expression, disableStaticComputedKey);
+        }
+
+        return false;
+    }
+
+    switch (left.type) {
+        case "Super":
+        case "ThisExpression":
+            return true;
+
+        case "Identifier":
+            return left.name === right.name;
+        case "Literal":
+            return equalLiteralValue(left, right);
+
+        case "ChainExpression":
+            return isSameReference(left.expression, right.expression, disableStaticComputedKey);
+
+        case "MemberExpression": {
+            if (!disableStaticComputedKey) {
+                const nameA = getStaticPropertyName(left);
+
+                // x.y = x["y"]
+                if (nameA !== null) {
+                    return (
+                        isSameReference(left.object, right.object, disableStaticComputedKey) &&
+                        nameA === getStaticPropertyName(right)
+                    );
+                }
+            }
+
+            /*
+             * x[0] = x[0]
+             * x[y] = x[y]
+             * x.y = x.y
+             */
+            return (
+                left.computed === right.computed &&
+                isSameReference(left.object, right.object, disableStaticComputedKey) &&
+                isSameReference(left.property, right.property, disableStaticComputedKey)
+            );
+        }
+
+        default:
+            return false;
+    }
+}
+
 /**
  * Checks whether or not a node is `Reflect.apply`.
  * @param {ASTNode} node A node to check.
  * @returns {boolean} Whether or not the node is a `Reflect.apply`.
  */
 function isReflectApply(node) {
-    return (
-        node.type === "MemberExpression" &&
-        node.object.type === "Identifier" &&
-        node.object.name === "Reflect" &&
-        node.property.type === "Identifier" &&
-        node.property.name === "apply" &&
-        node.computed === false
-    );
+    return isSpecificMemberAccess(node, "Reflect", "apply");
 }
 
 /**
@@ -188,14 +459,7 @@ function isReflectApply(node) {
  * @returns {boolean} Whether or not the node is a `Array.from`.
  */
 function isArrayFromMethod(node) {
-    return (
-        node.type === "MemberExpression" &&
-        node.object.type === "Identifier" &&
-        arrayOrTypedArrayPattern.test(node.object.name) &&
-        node.property.type === "Identifier" &&
-        node.property.name === "from" &&
-        node.computed === false
-    );
+    return isSpecificMemberAccess(node, arrayOrTypedArrayPattern, "from");
 }
 
 /**
@@ -204,17 +468,7 @@ function isArrayFromMethod(node) {
  * @returns {boolean} Whether or not the node is a method which has `thisArg`.
  */
 function isMethodWhichHasThisArg(node) {
-    for (
-        let currentNode = node;
-        currentNode.type === "MemberExpression" && !currentNode.computed;
-        currentNode = currentNode.property
-    ) {
-        if (currentNode.property.type === "Identifier") {
-            return arrayMethodPattern.test(currentNode.property.name);
-        }
-    }
-
-    return false;
+    return isSpecificMemberAccess(node, null, arrayMethodPattern);
 }
 
 /**
@@ -289,6 +543,15 @@ function isDotToken(token) {
     return token.value === "." && token.type === "Punctuator";
 }
 
+/**
+ * Checks if the given token is a `?.` token or not.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a `?.` token.
+ */
+function isQuestionDotToken(token) {
+    return token.value === "?." && token.type === "Punctuator";
+}
+
 /**
  * Checks if the given token is a semicolon token or not.
  * @param {Token} token The token to check.
@@ -463,6 +726,15 @@ function isMixedLogicalAndCoalesceExpressions(left, right) {
     );
 }
 
+/**
+ * Checks if the given operator is a logical assignment operator.
+ * @param {string} operator The operator to check.
+ * @returns {boolean} `true` if the operator is a logical assignment operator.
+ */
+function isLogicalAssignmentOperator(operator) {
+    return LOGICAL_ASSIGNMENT_OPERATORS.has(operator);
+}
+
 //------------------------------------------------------------------------------
 // Public Interface
 //------------------------------------------------------------------------------
@@ -505,6 +777,7 @@ module.exports = {
     isCommaToken,
     isCommentToken,
     isDotToken,
+    isQuestionDotToken,
     isKeywordToken,
     isNotClosingBraceToken: negate(isClosingBraceToken),
     isNotClosingBracketToken: negate(isClosingBracketToken),
@@ -512,6 +785,7 @@ module.exports = {
     isNotColonToken: negate(isColonToken),
     isNotCommaToken: negate(isCommaToken),
     isNotDotToken: negate(isDotToken),
+    isNotQuestionDotToken: negate(isQuestionDotToken),
     isNotOpeningBraceToken: negate(isOpeningBraceToken),
     isNotOpeningBracketToken: negate(isOpeningBracketToken),
     isNotOpeningParenToken: negate(isOpeningParenToken),
@@ -669,6 +943,7 @@ module.exports = {
                  */
                 case "LogicalExpression":
                 case "ConditionalExpression":
+                case "ChainExpression":
                     currentNode = parent;
                     break;
 
@@ -755,14 +1030,21 @@ module.exports = {
                  *   (function foo() { ... }).apply(obj, []);
                  */
                 case "MemberExpression":
-                    return (
-                        parent.object !== currentNode ||
-                        parent.property.type !== "Identifier" ||
-                        !bindOrCallOrApplyPattern.test(parent.property.name) ||
-                        !isCallee(parent) ||
-                        parent.parent.arguments.length === 0 ||
-                        isNullOrUndefined(parent.parent.arguments[0])
-                    );
+                    if (
+                        parent.object === currentNode &&
+                        isSpecificMemberAccess(parent, null, bindOrCallOrApplyPattern)
+                    ) {
+                        const maybeCalleeNode = parent.parent.type === "ChainExpression"
+                            ? parent.parent
+                            : parent;
+
+                        return !(
+                            isCallee(maybeCalleeNode) &&
+                            maybeCalleeNode.parent.arguments.length >= 1 &&
+                            !isNullOrUndefined(maybeCalleeNode.parent.arguments[0])
+                        );
+                    }
+                    return true;
 
                 /*
                  * e.g.
@@ -884,6 +1166,7 @@ module.exports = {
                 return 17;
 
             case "CallExpression":
+            case "ChainExpression":
             case "ImportExpression":
                 return 18;
 
@@ -913,104 +1196,6 @@ module.exports = {
         return isFunction(node) && module.exports.isEmptyBlock(node.body);
     },
 
-    /**
-     * Returns the result of the string conversion applied to the evaluated value of the given expression node,
-     * if it can be determined statically.
-     *
-     * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only.
-     * In all other cases, this function returns `null`.
-     * @param {ASTNode} node Expression node.
-     * @returns {string|null} String value if it can be determined. Otherwise, `null`.
-     */
-    getStaticStringValue(node) {
-        switch (node.type) {
-            case "Literal":
-                if (node.value === null) {
-                    if (module.exports.isNullLiteral(node)) {
-                        return String(node.value); // "null"
-                    }
-                    if (node.regex) {
-                        return `/${node.regex.pattern}/${node.regex.flags}`;
-                    }
-                    if (node.bigint) {
-                        return node.bigint;
-                    }
-
-                    // Otherwise, this is an unknown literal. The function will return null.
-
-                } else {
-                    return String(node.value);
-                }
-                break;
-            case "TemplateLiteral":
-                if (node.expressions.length === 0 && node.quasis.length === 1) {
-                    return node.quasis[0].value.cooked;
-                }
-                break;
-
-            // no default
-        }
-
-        return null;
-    },
-
-    /**
-     * Gets the property name of a given node.
-     * The node can be a MemberExpression, a Property, or a MethodDefinition.
-     *
-     * If the name is dynamic, this returns `null`.
-     *
-     * For examples:
-     *
-     *     a.b           // => "b"
-     *     a["b"]        // => "b"
-     *     a['b']        // => "b"
-     *     a[`b`]        // => "b"
-     *     a[100]        // => "100"
-     *     a[b]          // => null
-     *     a["a" + "b"]  // => null
-     *     a[tag`b`]     // => null
-     *     a[`${b}`]     // => null
-     *
-     *     let a = {b: 1}            // => "b"
-     *     let a = {["b"]: 1}        // => "b"
-     *     let a = {['b']: 1}        // => "b"
-     *     let a = {[`b`]: 1}        // => "b"
-     *     let a = {[100]: 1}        // => "100"
-     *     let a = {[b]: 1}          // => null
-     *     let a = {["a" + "b"]: 1}  // => null
-     *     let a = {[tag`b`]: 1}     // => null
-     *     let a = {[`${b}`]: 1}     // => null
-     * @param {ASTNode} node The node to get.
-     * @returns {string|null} The property name if static. Otherwise, null.
-     */
-    getStaticPropertyName(node) {
-        let prop;
-
-        switch (node && node.type) {
-            case "Property":
-            case "MethodDefinition":
-                prop = node.key;
-                break;
-
-            case "MemberExpression":
-                prop = node.property;
-                break;
-
-            // no default
-        }
-
-        if (prop) {
-            if (prop.type === "Identifier" && !node.computed) {
-                return prop.name;
-            }
-
-            return module.exports.getStaticStringValue(prop);
-        }
-
-        return null;
-    },
-
     /**
      * Get directives from directive prologue of a Program or Function node.
      * @param {ASTNode} node The node to check.
@@ -1056,16 +1241,27 @@ module.exports = {
      * @returns {boolean} `true` if this node is a decimal integer.
      * @example
      *
-     * 5       // true
-     * 5.      // false
-     * 5.0     // false
-     * 05      // false
-     * 0x5     // false
-     * 0b101   // false
-     * 0o5     // false
-     * 5e0     // false
-     * '5'     // false
-     * 5n      // false
+     * 0         // true
+     * 5         // true
+     * 50        // true
+     * 5_000     // true
+     * 1_234_56  // true
+     * 08        // true
+     * 0192      // true
+     * 5.        // false
+     * .5        // false
+     * 5.0       // false
+     * 5.00_00   // false
+     * 05        // false
+     * 0x5       // false
+     * 0b101     // false
+     * 0b11_01   // false
+     * 0o5       // false
+     * 5e0       // false
+     * 5e1_000   // false
+     * 5n        // false
+     * 1_000n    // false
+     * '5'       // false
      */
     isDecimalInteger(node) {
         return node.type === "Literal" && typeof node.value === "number" &&
@@ -1164,7 +1360,7 @@ module.exports = {
         if (node.id) {
             tokens.push(`'${node.id.name}'`);
         } else {
-            const name = module.exports.getStaticPropertyName(parent);
+            const name = getStaticPropertyName(parent);
 
             if (name !== null) {
                 tokens.push(`'${name}'`);
@@ -1391,10 +1587,24 @@ module.exports = {
             case "TaggedTemplateExpression":
             case "YieldExpression":
             case "AwaitExpression":
+            case "ChainExpression":
                 return true; // possibly an error object.
 
             case "AssignmentExpression":
-                return module.exports.couldBeError(node.right);
+                if (["=", "&&="].includes(node.operator)) {
+                    return module.exports.couldBeError(node.right);
+                }
+
+                if (["||=", "??="].includes(node.operator)) {
+                    return module.exports.couldBeError(node.left) || module.exports.couldBeError(node.right);
+                }
+
+                /**
+                 * All other assignment operators are mathematical assignment operators (arithmetic or bitwise).
+                 * An assignment expression with a mathematical operator can either evaluate to a primitive value,
+                 * or throw, depending on the operands. Thus, it cannot evaluate to an `Error` object.
+                 */
+                return false;
 
             case "SequenceExpression": {
                 const exprs = node.expressions;
@@ -1403,6 +1613,17 @@ module.exports = {
             }
 
             case "LogicalExpression":
+
+                /*
+                 * If the && operator short-circuits, the left side was falsy and therefore not an error, and if it
+                 * doesn't short-circuit, it takes the value from the right side, so the right side must always be
+                 * a plausible error. A future improvement could verify that the left side could be truthy by
+                 * excluding falsy literals.
+                 */
+                if (node.operator === "&&") {
+                    return module.exports.couldBeError(node.right);
+                }
+
                 return module.exports.couldBeError(node.left) || module.exports.couldBeError(node.right);
 
             case "ConditionalExpression":
@@ -1413,23 +1634,6 @@ module.exports = {
         }
     },
 
-    /**
-     * Determines whether the given node is a `null` literal.
-     * @param {ASTNode} node The node to check
-     * @returns {boolean} `true` if the node is a `null` literal
-     */
-    isNullLiteral(node) {
-
-        /*
-         * Checking `node.value === null` does not guarantee that a literal is a null literal.
-         * When parsing values that cannot be represented in the current environment (e.g. unicode
-         * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to
-         * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check
-         * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020
-         */
-        return node.type === "Literal" && node.value === null && !node.regex && !node.bigint;
-    },
-
     /**
      * Check if a given node is a numeric literal or not.
      * @param {ASTNode} node The node to check.
@@ -1575,20 +1779,31 @@ module.exports = {
     },
 
     /**
-     * Determines whether the given raw string contains an octal escape sequence.
+     * Determines whether the given raw string contains an octal escape sequence
+     * or a non-octal decimal escape sequence ("\8", "\9").
      *
-     * "\1", "\2" ... "\7"
-     * "\00", "\01" ... "\09"
+     * "\1", "\2" ... "\7", "\8", "\9"
+     * "\00", "\01" ... "\07", "\08", "\09"
      *
      * "\0", when not followed by a digit, is not an octal escape sequence.
      * @param {string} rawString A string in its raw representation.
-     * @returns {boolean} `true` if the string contains at least one octal escape sequence.
+     * @returns {boolean} `true` if the string contains at least one octal escape sequence
+     * or at least one non-octal decimal escape sequence.
      */
-    hasOctalEscapeSequence(rawString) {
-        return OCTAL_ESCAPE_PATTERN.test(rawString);
+    hasOctalOrNonOctalDecimalEscapeSequence(rawString) {
+        return OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN.test(rawString);
     },
 
     isLogicalExpression,
     isCoalesceExpression,
-    isMixedLogicalAndCoalesceExpressions
+    isMixedLogicalAndCoalesceExpressions,
+    isNullLiteral,
+    getStaticStringValue,
+    getStaticPropertyName,
+    skipChainExpression,
+    isSpecificId,
+    isSpecificMemberAccess,
+    equalLiteralValue,
+    isSameReference,
+    isLogicalAssignmentOperator
 };