Repository: eslint-community/eslint-plugin-es-x
Pull request #39 (2022-11-30 10:19:01)

Add `no-error-cause` rule
This PR is from #24 with additional fixes.
~~Fixed the rule and excluded it from the config (because changing the config is a breaking change).~~

close #24


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..bb85b6f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: daily
+
+  - package-ecosystem: npm
+    directory: /
+    schedule:
+      interval: daily
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0abf7c4..137fe1e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,8 +16,6 @@ jobs:
         uses: actions/checkout@v3
       - name: Install Node.js
         uses: actions/setup-node@v3
-        with:
-          node-version: 14
       - name: Install Packages
         run: npm install
       - name: Lint
@@ -28,43 +26,46 @@ jobs:
 
     strategy:
       matrix:
-        eslint: [8]
-        node: [16]
-        os: [ubuntu-latest]
         include:
+          - eslint: 8
+            node: 18
+            os: ubuntu-latest
           # On other platforms
           - eslint: 8
-            node: 16
+            node: 18
             os: windows-latest
           - eslint: 8
-            node: 16
+            node: 18
             os: macos-latest
-          # On old Node.js versions
+          # On other Node.js versions
           - eslint: 8
-            node: 14
+            node: 19
             os: ubuntu-latest
-          - eslint: 7
-            node: 12
+          - eslint: 8
+            node: 16
             os: ubuntu-latest
-          - eslint: 7
-            node: 10
+          - eslint: 8
+            node: 17
+            os: ubuntu-latest
+          - eslint: 8
+            node: 14
             os: ubuntu-latest
           # On old ESLint versions
           - eslint: 7
-            node: 16
+            node: 18
             os: ubuntu-latest
           - eslint: 6
-            node: 16
+            node: 18
             os: ubuntu-latest
           - eslint: 5
-            node: 16
+            node: 18
             os: ubuntu-latest
           - eslint: 4
-            node: 16
+            node: 18
             os: ubuntu-latest
           # On the minimum supported ESLint/Node.js version
           - eslint: 4
-            node: "8.10"
+            node: 14.18.0
             os: ubuntu-latest
 
     runs-on: ${{ matrix.os }}
@@ -78,14 +79,6 @@ jobs:
       - name: Uninstall Packages
         run: |+
             npm r -D vuepress eslint-plugin-eslint-plugin eslint-plugin-prettier vue-eslint-parser eslint-plugin-vue
-      - name: Install for Node v8
-        run: |+
-            npm i -D @typescript-eslint/parser@3 mocha@7
-        if: "matrix.node == '8.10'"
-      - name: Install for Node v10
-        run: |+
-            npm i -D mocha@9
-        if: "matrix.node == 10"
       - name: Install for ESLint v4
         run: |+
             npm i -D eslint-utils@2 --legacy-peer-deps
@@ -100,9 +93,9 @@ jobs:
             npx rimraf node_modules
       - name: Install Packages
         run: npm install
-        if: "matrix.eslint != 4 || matrix.node == '8.10'"
+        if: "matrix.eslint != 4"
       - name: Install Packages --legacy-peer-deps
         run: npm install --legacy-peer-deps
-        if: "matrix.eslint == 4 && matrix.node != '8.10'"
+        if: "matrix.eslint == 4"
       - name: Test
         run: npm run -s test:mocha
diff --git a/docs/README.md b/docs/README.md
index caa240e..b42bab8 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -25,7 +25,7 @@ npm install --save-dev eslint eslint-plugin-es-x
 ```
 
 ::: tip Requirements
-- Node.js `8.10.0` or newer.
+- Node.js `14.18.0` or newer, except `15.x`.
 - ESLint `4.19.1` or newer.
 :::
 
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e5b6e70..2b36a1c 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -23,6 +23,7 @@ There are multiple configs that enable all rules in this category: `plugin:es-x/
 | [es-x/no-array-string-prototype-at](./no-array-string-prototype-at.md) | disallow the `{Array,String}.prototype.at()` methods. |  |
 | [es-x/no-class-fields](./no-class-fields.md) | disallow class fields. |  |
 | [es-x/no-class-static-block](./no-class-static-block.md) | disallow class static block. |  |
+| [es-x/no-error-cause](./no-error-cause.md) | disallow Error Cause. |  |
 | [es-x/no-object-hasown](./no-object-hasown.md) | disallow the `Object.hasOwn` method. |  |
 | [es-x/no-private-in](./no-private-in.md) | disallow `#x in obj`. |  |
 | [es-x/no-regexp-d-flag](./no-regexp-d-flag.md) | disallow RegExp `d` flag. |  |
diff --git a/docs/rules/no-error-cause.md b/docs/rules/no-error-cause.md
new file mode 100644
index 0000000..992816b
--- /dev/null
+++ b/docs/rules/no-error-cause.md
@@ -0,0 +1,30 @@
+---
+title: "es-x/no-error-cause"
+description: "disallow Error Cause"
+---
+
+# es-x/no-error-cause
+> disallow Error Cause
+
+- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- ✅ The following configurations enable this rule: `plugin:es-x/no-new-in-es2022`, `plugin:es-x/restrict-to-es3`, `plugin:es-x/restrict-to-es5`, `plugin:es-x/restrict-to-es2015`, `plugin:es-x/restrict-to-es2016`, `plugin:es-x/restrict-to-es2017`, `plugin:es-x/restrict-to-es2018`, `plugin:es-x/restrict-to-es2019`, `plugin:es-x/restrict-to-es2020`, and `plugin:es-x/restrict-to-es2021`
+
+This rule reports ES2022 [Error Cause](https://github.com/tc39/proposal-error-cause).
+
+## 💡 Examples
+
+⛔ Examples of **incorrect** code for this rule:
+
+<eslint-playground type="bad">
+
+```js
+/*eslint es-x/no-error-cause: error */
+throw new Error('failed', { cause: err });
+```
+
+</eslint-playground>
+
+## 📚 References
+
+- [Rule source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/lib/rules/no-error-cause.js)
+- [Test source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/tests/lib/rules/no-error-cause.js)
diff --git a/lib/configs/no-new-in-es2022.js b/lib/configs/no-new-in-es2022.js
index 4e78303..27d020e 100644
--- a/lib/configs/no-new-in-es2022.js
+++ b/lib/configs/no-new-in-es2022.js
@@ -11,6 +11,7 @@ module.exports = {
         "es-x/no-array-string-prototype-at": "error",
         "es-x/no-class-fields": "error",
         "es-x/no-class-static-block": "error",
+        "es-x/no-error-cause": "error",
         "es-x/no-object-hasown": "error",
         "es-x/no-private-in": "error",
         "es-x/no-regexp-d-flag": "error",
diff --git a/lib/index.js b/lib/index.js
index 8dfdcc5..09db08b 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -96,6 +96,7 @@ module.exports = {
         "no-default-parameters": require("./rules/no-default-parameters"),
         "no-destructuring": require("./rules/no-destructuring"),
         "no-dynamic-import": require("./rules/no-dynamic-import"),
+        "no-error-cause": require("./rules/no-error-cause"),
         "no-escape-unescape": require("./rules/no-escape-unescape"),
         "no-exponential-operators": require("./rules/no-exponential-operators"),
         "no-export-ns-from": require("./rules/no-export-ns-from"),
diff --git a/lib/rules/no-error-cause.js b/lib/rules/no-error-cause.js
new file mode 100644
index 0000000..36a957d
--- /dev/null
+++ b/lib/rules/no-error-cause.js
@@ -0,0 +1,186 @@
+/**
+ * @author Sosuke Suzuki <https://github.com/sosukesuzuki>
+ * See LICENSE file in root directory for full license.
+ */
+
+"use strict"
+
+const {
+    CONSTRUCT,
+    READ,
+    ReferenceTracker,
+    getPropertyName,
+} = require("eslint-utils")
+
+/**
+ * @typedef {import("estree").Node} Node
+ * @typedef {import("estree").ClassExpression | import("estree").ClassDeclaration} ClassNode
+ * @typedef {import("estree").CallExpression} CallExpression
+ */
+
+const errorConstructorNames = [
+    "Error",
+    "EvalError",
+    "RangeError",
+    "ReferenceError",
+    "SyntaxError",
+    "TypeError",
+    "URIError",
+    "AggregateError",
+]
+const errorsTraceMap = {}
+for (const errorConstructorName of errorConstructorNames) {
+    errorsTraceMap[errorConstructorName] = { [CONSTRUCT]: true, [READ]: true }
+}
+
+/**
+ * @param {Node} node
+ * @returns {boolean}
+ */
+function isSuperCall(node) {
+    return node.type === "CallExpression" && node.callee.type === "Super"
+}
+
+/**
+ * @param {Node|undefined} node
+ * @returns {boolean}
+ */
+function isSpreadElement(node) {
+    return node && node.type === "SpreadElement"
+}
+
+/**
+ * @param {Node} node
+ * @returns {ClassNode | null}
+ */
+function findClassFromAncestors(node) {
+    if (node.type !== "ClassExpression" && node.type !== "ClassDeclaration") {
+        return findClassFromAncestors(node.parent)
+    }
+    if (!node) {
+        return null
+    }
+    return node
+}
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow Error Cause.",
+            category: "ES2022",
+            recommended: false,
+            url: "http://eslint-community.github.io/eslint-plugin-es-x/rules/no-error-cause.html",
+        },
+        fixable: null,
+        messages: {
+            forbidden: "ES2022 Error Cause is forbidden.",
+        },
+        schema: [],
+        type: "problem",
+    },
+    create(context) {
+        /** @type {Array<{ classNode: ClassNode, superCallNode: CallExpression }>} */
+        const maybeErrorSubclasses = []
+
+        /** @type {Array<{ classNode: ClassNode, superCallNode: CallExpression }>} */
+        const maybeAggregateErrorSubclasses = []
+
+        /**
+         * Checks if the received node is a constructor call with cause option.
+         * e.g. `new Error("message", { cause: foo })`, `super("message", { cause: foo })`
+         *
+         * @param {Node} node
+         * @param {boolean} isAggregateError
+         * @returns {boolean}
+         */
+        function isConstructCallWithCauseOption(node, isAggregateError) {
+            if (node.type !== "NewExpression" && !isSuperCall(node)) {
+                return false
+            }
+            const optionsArgIndex = isAggregateError ? 2 : 1
+            for (let index = 0; index < optionsArgIndex; index++) {
+                if (isSpreadElement(node.arguments[index])) {
+                    return false
+                }
+            }
+            const optionsArg = node.arguments[optionsArgIndex]
+            if (!optionsArg || optionsArg.type !== "ObjectExpression") {
+                return false
+            }
+            return optionsArg.properties.some((property) => {
+                if (property.type !== "Property") {
+                    return false
+                }
+                // new Error("msg", { cause: foo })
+                return getPropertyName(property, context.getScope()) === "cause"
+            })
+        }
+
+        /**
+         * @param {Node} node
+         * @param {isAggregateError} boolean
+         * @return {Node | null}
+         */
+        function getReportedNode(node, isAggregateError) {
+            const errorSubclasses = isAggregateError
+                ? maybeAggregateErrorSubclasses
+                : maybeErrorSubclasses
+
+            if (errorSubclasses.length > 0) {
+                for (const { classNode, superCallNode } of errorSubclasses) {
+                    if (classNode.superClass === node) {
+                        return superCallNode
+                    }
+                }
+            }
+            if (isConstructCallWithCauseOption(node, isAggregateError)) {
+                return node
+            }
+            return null
+        }
+
+        return {
+            Super(node) {
+                const superCallNode = node.parent
+
+                function findErrorSubclasses(isAggregateError) {
+                    const errorSubclasses = isAggregateError
+                        ? maybeAggregateErrorSubclasses
+                        : maybeErrorSubclasses
+
+                    if (
+                        isConstructCallWithCauseOption(
+                            superCallNode,
+                            isAggregateError,
+                        )
+                    ) {
+                        const classNode = findClassFromAncestors(superCallNode)
+                        if (classNode && classNode.superClass) {
+                            errorSubclasses.push({ classNode, superCallNode })
+                        }
+                    }
+                }
+
+                findErrorSubclasses(/* isAggregateError */ false)
+                findErrorSubclasses(/* isAggregateError */ true)
+            },
+            "Program:exit"() {
+                const tracker = new ReferenceTracker(context.getScope())
+                for (const { node, path } of tracker.iterateGlobalReferences(
+                    errorsTraceMap,
+                )) {
+                    const reportedNode = getReportedNode(
+                        node,
+                        path.join(",") === "AggregateError",
+                    )
+                    if (reportedNode) {
+                        context.report({
+                            node: reportedNode,
+                            messageId: "forbidden",
+                        })
+                    }
+                }
+            },
+        }
+    },
+}
diff --git a/package.json b/package.json
index a16a1cb..f1d1e0c 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "version": "5.4.0",
   "description": "ESLint plugin about ECMAScript syntactic features.",
   "engines": {
-    "node": ">=8.10.0"
+    "node": "^14.18.0 || >=16.0.0"
   },
   "main": "lib/index.js",
   "files": [
diff --git a/scripts/rules.js b/scripts/rules.js
index 2f0b2f3..0c84c6f 100644
--- a/scripts/rules.js
+++ b/scripts/rules.js
@@ -74,11 +74,17 @@ const rules = []
             .replace(/\.js$/u, "")
             .replace(/\\/gu, "/")
         const content = fs.readFileSync(filePath, "utf8")
+        const contentWithoutComments = content.replace(
+            /("(?:\\"|[^"])*?"|'(?:\\'|[^'])*?')|\/\/[^\n]*|\/\*[\s\S]*?\*\//gu,
+            "$1",
+        )
         const category = /category:[\s\n]+(?:undefined|"(.+)")/u.exec(
-            content,
+            contentWithoutComments,
         )[1]
-        const description = /description:[\s\n]+"(.+?)\.?"/u.exec(content)[1]
-        const fixable = /fixable:[\s\n]+"(.+)"/u.test(content)
+        const description = /description:[\s\n]+"(.+?)\.?"/u.exec(
+            contentWithoutComments,
+        )[1]
+        const fixable = /fixable:[\s\n]+"(.+)"/u.test(contentWithoutComments)
         const rule = {
             ruleId,
             description: JSON.parse(`"${description}"`),


Test patch:
diff --git a/tests/lib/rules/no-error-cause.js b/tests/lib/rules/no-error-cause.js
new file mode 100644
index 0000000..7c77874
--- /dev/null
+++ b/tests/lib/rules/no-error-cause.js
@@ -0,0 +1,77 @@
+/**
+ * @author Sosuke Suzuki <https://github.com/sosukesuzuki>
+ * See LICENSE file in root directory for full license.
+ */
+
+"use strict"
+
+const RuleTester = require("../../tester")
+const rule = require("../../../lib/rules/no-error-cause.js")
+
+if (!RuleTester.isSupported(2022)) {
+    //eslint-disable-next-line no-console
+    console.log("Skip the tests of no-error-cause.")
+    return
+}
+
+const errorConstructorNames = [
+    "Error",
+    "AggregateError",
+    "EvalError",
+    "RangeError",
+    "ReferenceError",
+    "SyntaxError",
+    "TypeError",
+    "URIError",
+]
+
+function getErrors(errorConstructorName) {
+    return errorConstructorName === "AggregateError"
+        ? '[new Error("message")], '
+        : ""
+}
+
+const valid = errorConstructorNames
+    .map((errorConstructorName) => {
+        const errors = getErrors(errorConstructorName)
+        return [
+            `new ${errorConstructorName}(${errors}"message")`,
+            `new ${errorConstructorName}(${errors}"message", notObjectExpression)`,
+            `new ${errorConstructorName}(${errors}"message", { notCause: foo })`,
+            `new ${errorConstructorName}(${errors}...foo, { cause: foo });`,
+            `class MyError extends ${errorConstructorName} { constructor() { super(${errors}"message") } }`,
+            `class MyError extends ${errorConstructorName} { constructor() { super(${errors}"message", notObjectExpression) } }`,
+            `class MyError extends ${errorConstructorName} { constructor() { super(${errors}"message", { notCause: foo }) } }`,
+        ]
+    })
+    // alternative of `Array.prototype.flat`
+    .reduce((acc, val) => acc.concat(val), [])
+
+const invalid = errorConstructorNames
+    .map((errorConstructorName) => {
+        const errors = getErrors(errorConstructorName)
+        return [
+            {
+                code: `new ${errorConstructorName}(${errors}"message", { cause: foo });`,
+                errors: ["ES2022 Error Cause is forbidden."],
+            },
+            {
+                code: `new ${errorConstructorName}(${errors}"message", { ["cause"]: foo });`,
+                errors: ["ES2022 Error Cause is forbidden."],
+            },
+            {
+                code: `const MyError = ${errorConstructorName}; new MyError(${errors}"message", { ["cause"]: foo });`,
+                errors: ["ES2022 Error Cause is forbidden."],
+            },
+            {
+                code: `class MyError extends ${errorConstructorName} { constructor() { super(${errors}"message", { cause: foo }); } }`,
+                errors: ["ES2022 Error Cause is forbidden."],
+            },
+        ]
+    })
+    // alternative of `Array.prototype.flat`
+    .reduce((acc, val) => acc.concat(val), [])
+
+new RuleTester({
+    parserOptions: { sourceType: "module" },
+}).run("no-error-cause", rule, { valid, invalid })