Repository: eslint-community/eslint-plugin-es-x (js)

Issue:
Add rule for RegExp duplicate named capture groups (ES2025)
https://github.com/tc39/proposal-duplicate-named-capturing-groups

Pull request:
Add `es-x/no-regexp-duplicate-named-capturing-groups` rule
close #142

Patch:
diff --git a/docs/rules/no-regexp-duplicate-named-capturing-groups.md b/docs/rules/no-regexp-duplicate-named-capturing-groups.md
new file mode 100644
index 0000000..6c6f185
--- /dev/null
+++ b/docs/rules/no-regexp-duplicate-named-capturing-groups.md
@@ -0,0 +1,30 @@
+---
+title: "es-x/no-regexp-duplicate-named-capturing-groups"
+description: "disallow RegExp duplicate named capturing groups"
+---
+
+# es-x/no-regexp-duplicate-named-capturing-groups
+> disallow RegExp duplicate named capturing groups
+
+- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+This rule reports ES2025 [RegExp duplicate named capture groups](https://github.com/tc39/proposal-duplicate-named-capturing-groups) as errors.
+
+## 💡 Examples
+
+⛔ Examples of **incorrect** code for this rule:
+
+<eslint-playground type="bad">
+
+```js
+/*eslint es-x/no-regexp-duplicate-named-capturing-groups: error */
+const r1 = /(?<year>\d{4})-\d{2}|\d{2}-(?<year>\d{4})/
+const r2 = /(?<x>a)|(?<x>b)/
+```
+
+</eslint-playground>
+
+## 📚 References
+
+- [Rule source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/lib/rules/no-regexp-duplicate-named-capturing-groups.js)
+- [Test source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/tests/lib/rules/no-regexp-duplicate-named-capturing-groups.js)
diff --git a/lib/rules/no-regexp-duplicate-named-capturing-groups.js b/lib/rules/no-regexp-duplicate-named-capturing-groups.js
new file mode 100644
index 0000000..59daf20
--- /dev/null
+++ b/lib/rules/no-regexp-duplicate-named-capturing-groups.js
@@ -0,0 +1,71 @@
+"use strict"
+
+const { getSourceCode } = require("eslint-compat-utils")
+const { defineRegExpHandler } = require("../util/define-regexp-handler")
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow RegExp duplicate named capturing groups.",
+            category: "ES2025",
+            recommended: false,
+            url: "http://eslint-community.github.io/eslint-plugin-es-x/rules/no-regexp-duplicate-named-capturing-groups.html",
+        },
+        fixable: null,
+        messages: {
+            forbidden:
+                "ES2025 RegExp duplicate named capturing groups are forbidden.",
+        },
+        schema: [],
+        type: "problem",
+    },
+    create(context) {
+        return defineRegExpHandler(context, (node) => {
+            const found = new Map()
+            return {
+                onPatternEnter() {
+                    found.clear()
+                },
+                onCapturingGroupLeave(start, end, name) {
+                    if (!name) {
+                        return
+                    }
+                    const list = found.get(name)
+                    if (list) {
+                        list.push({ start, end })
+                    } else {
+                        found.set(name, [{ start, end }])
+                    }
+                },
+                onExit() {
+                    for (const [, dupe] of found.values()) {
+                        if (!dupe) {
+                            continue
+                        }
+                        const { start, end } = dupe
+                        const sourceCode = getSourceCode(context)
+                        context.report({
+                            node,
+                            loc:
+                                node.type === "Literal"
+                                    ? {
+                                          start: sourceCode.getLocFromIndex(
+                                              node.range[0] +
+                                                  1 /* slash */ +
+                                                  start,
+                                          ),
+                                          end: sourceCode.getLocFromIndex(
+                                              node.range[0] +
+                                                  1 /* slash */ +
+                                                  end,
+                                          ),
+                                      }
+                                    : null,
+                            messageId: "forbidden",
+                        })
+                    }
+                },
+            }
+        })
+    },
+}
diff --git a/package.json b/package.json
index b6c9c37..6ef3221 100644
--- a/package.json
+++ b/package.json
@@ -14,12 +14,11 @@
   },
   "dependencies": {
     "@eslint-community/eslint-utils": "^4.1.2",
-    "@eslint-community/regexpp": "^4.6.0",
+    "@eslint-community/regexpp": "^4.11.0",
     "eslint-compat-utils": "^0.5.1"
   },
   "devDependencies": {
     "@typescript-eslint/parser": "^7.0.2",
-    "acorn": "^8.7.0",
     "env-cmd": "^10.1.0",
     "eslint": "^9.1.0",
     "eslint-plugin-eslint-comments": "^3.2.0",
@@ -27,7 +26,7 @@
     "eslint-plugin-n": "^17.8.1",
     "eslint-plugin-prettier": "^5.0.0",
     "eslint-plugin-vue": "^9.25.0",
-    "espree": "^10.0.1",
+    "espree": "^10.1.0",
     "globals": "^15.0.0",
     "jsdom": "^24.0.0",
     "mocha": "^10.0.0",


Test patch:
diff --git a/tests/lib/rules/no-regexp-duplicate-named-capturing-groups.js b/tests/lib/rules/no-regexp-duplicate-named-capturing-groups.js
new file mode 100644
index 0000000..f8e1c3c
--- /dev/null
+++ b/tests/lib/rules/no-regexp-duplicate-named-capturing-groups.js
@@ -0,0 +1,77 @@
+"use strict"
+
+const RuleTester = require("../../tester")
+const rule = require("../../../lib/rules/no-regexp-duplicate-named-capturing-groups.js")
+
+if (!RuleTester.isSupported(2025)) {
+    //eslint-disable-next-line no-console
+    console.log("Skip the tests of no-regexp-duplicate-named-capturing-groups.")
+    return
+}
+
+new RuleTester().run("no-regexp-duplicate-named-capturing-groups", rule, {
+    valid: [
+        String.raw`/(?<x>a)/`,
+        String.raw`/(a)/`,
+        String.raw`/(?<x>a)(?<y>b)/`,
+        String.raw`/(?<x>a)(b)/`,
+        String.raw`/(?<x>a)|(?<y>b)/`,
+        String.raw`new RegExp("(?<x>a)")`,
+        String.raw`new RegExp("(a)")`,
+        String.raw`new RegExp("(?<x>a)(?<y>b)")`,
+        String.raw`new RegExp("(?<x>a)(b)")`,
+        String.raw`new RegExp("(?<x>a)|(?<y>b)")`,
+    ],
+    invalid: [
+        {
+            code: String.raw`/(?<x>a)|(?<x>b)/`,
+            errors: [
+                {
+                    message:
+                        "ES2025 RegExp duplicate named capturing groups are forbidden.",
+                    column: 10,
+                },
+            ],
+        },
+        {
+            code: String.raw`/(?<x>a)|(?<x>b)|(?<x>c)/`,
+            errors: [
+                {
+                    message:
+                        "ES2025 RegExp duplicate named capturing groups are forbidden.",
+                    column: 10,
+                },
+            ],
+        },
+        {
+            code: String.raw`new RegExp("(?<x>a)|(?<x>b)")`,
+            errors: [
+                {
+                    message:
+                        "ES2025 RegExp duplicate named capturing groups are forbidden.",
+                    column: 1,
+                },
+            ],
+        },
+        {
+            code: String.raw`new RegExp("(?<x>a)|(?<x>b)|(?<x>c)")`,
+            errors: [
+                {
+                    message:
+                        "ES2025 RegExp duplicate named capturing groups are forbidden.",
+                    column: 1,
+                },
+            ],
+        },
+        {
+            code: String.raw`/(?<x>a)|(?<y>b)|(?<x>c)/`,
+            errors: [
+                {
+                    message:
+                        "ES2025 RegExp duplicate named capturing groups are forbidden.",
+                    column: 18,
+                },
+            ],
+        },
+    ],
+})
diff --git a/tests/tester.js b/tests/tester.js
index 7cacd2e..d77a1fc 100644
--- a/tests/tester.js
+++ b/tests/tester.js
@@ -13,6 +13,7 @@ const RuleTester = getRuleTester()
 const eslintVersion = new Linter().version
 const ecmaVersion =
     /*eslint-disable prettier/prettier */
+    semver.gte(eslintVersion, "9.6.0") ? 2025 :
     semver.gte(eslintVersion, "8.44.0") ? 2024 :
     semver.gte(eslintVersion, "8.23.0") ? 2023 :
     semver.gte(eslintVersion, "8.0.0") ? 2022 :