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 :