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 00000000..6c6f1854
--- /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
+
+- ❗ ***This rule has not been released yet.***
+
+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:
+
+
+
+```js
+/*eslint es-x/no-regexp-duplicate-named-capturing-groups: error */
+const r1 = /(?\d{4})-\d{2}|\d{2}-(?\d{4})/
+const r2 = /(?a)|(?b)/
+```
+
+
+
+## 📚 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 00000000..59daf207
--- /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 b6c9c373..6ef32217 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",
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 00000000..f8e1c3c6
--- /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`/(?a)/`,
+ String.raw`/(a)/`,
+ String.raw`/(?a)(?b)/`,
+ String.raw`/(?a)(b)/`,
+ String.raw`/(?a)|(?b)/`,
+ String.raw`new RegExp("(?a)")`,
+ String.raw`new RegExp("(a)")`,
+ String.raw`new RegExp("(?a)(?b)")`,
+ String.raw`new RegExp("(?a)(b)")`,
+ String.raw`new RegExp("(?a)|(?b)")`,
+ ],
+ invalid: [
+ {
+ code: String.raw`/(?a)|(?b)/`,
+ errors: [
+ {
+ message:
+ "ES2025 RegExp duplicate named capturing groups are forbidden.",
+ column: 10,
+ },
+ ],
+ },
+ {
+ code: String.raw`/(?a)|(?b)|(?c)/`,
+ errors: [
+ {
+ message:
+ "ES2025 RegExp duplicate named capturing groups are forbidden.",
+ column: 10,
+ },
+ ],
+ },
+ {
+ code: String.raw`new RegExp("(?a)|(?b)")`,
+ errors: [
+ {
+ message:
+ "ES2025 RegExp duplicate named capturing groups are forbidden.",
+ column: 1,
+ },
+ ],
+ },
+ {
+ code: String.raw`new RegExp("(?a)|(?b)|(?c)")`,
+ errors: [
+ {
+ message:
+ "ES2025 RegExp duplicate named capturing groups are forbidden.",
+ column: 1,
+ },
+ ],
+ },
+ {
+ code: String.raw`/(?a)|(?b)|(?c)/`,
+ errors: [
+ {
+ message:
+ "ES2025 RegExp duplicate named capturing groups are forbidden.",
+ column: 18,
+ },
+ ],
+ },
+ ],
+})
diff --git a/tests/tester.js b/tests/tester.js
index 7cacd2e1..d77a1fc2 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 :