Repository: ota-meshi/eslint-plugin-regexp (ts)

Issue:
Add auto-fix for `no-unused-capturing-group`?
Let's insert `?:`?

Pull request:
Add fix and suggestion for `no-unused-capturing-group`
This adds fixes and suggestions for the `no-unused-capturing-group` rule as discussed #230.

The rule will now always add suggestions where possible. 

Fixes will only be added if the new `fixable` option is set to `true`. The `fixable` options default to `false`, so fixing is opt-in. This option should be a decent compromise.

I also improved the error messages a little.

---

This fixes #230.

Patch:
diff --git a/README.md b/README.md
index 0a4094a..0c51ec9 100644
--- a/README.md
+++ b/README.md
@@ -137,7 +137,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-standalone-backslash](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-standalone-backslash.html) | disallow standalone backslashes (`\`) |  |
 | [regexp/no-trivially-nested-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-trivially-nested-assertion.html) | disallow trivially nested assertions | :wrench: |
 | [regexp/no-trivially-nested-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-trivially-nested-quantifier.html) | disallow nested quantifiers that can be rewritten as one quantifier | :wrench: |
-| [regexp/no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group |  |
+| [regexp/no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | :wrench: |
 | [regexp/no-useless-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-character-class.html) | disallow character class with one character | :wrench: |
 | [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
 | [regexp/no-useless-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-flag.html) | disallow unnecessary regex flags | :wrench: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index ee472b3..e5e8402 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -46,7 +46,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-standalone-backslash](./no-standalone-backslash.md) | disallow standalone backslashes (`\`) |  |
 | [regexp/no-trivially-nested-assertion](./no-trivially-nested-assertion.md) | disallow trivially nested assertions | :wrench: |
 | [regexp/no-trivially-nested-quantifier](./no-trivially-nested-quantifier.md) | disallow nested quantifiers that can be rewritten as one quantifier | :wrench: |
-| [regexp/no-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group |  |
+| [regexp/no-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group | :wrench: |
 | [regexp/no-useless-character-class](./no-useless-character-class.md) | disallow character class with one character | :wrench: |
 | [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
 | [regexp/no-useless-flag](./no-useless-flag.md) | disallow unnecessary regex flags | :wrench: |
diff --git a/docs/rules/no-unused-capturing-group.md b/docs/rules/no-unused-capturing-group.md
index fa0dc3b..1ad5f26 100644
--- a/docs/rules/no-unused-capturing-group.md
+++ b/docs/rules/no-unused-capturing-group.md
@@ -9,14 +9,16 @@ since: "v0.6.0"
 
 > disallow unused capturing group
 
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
 ## :book: Rule Details
 
 This rule aims to optimize regular expressions by replacing unused capturing groups with non-capturing groups.
 
-<eslint-code-block>
+<eslint-code-block fix>
 
 ```js
-/* eslint regexp/no-unused-capturing-group: "error" */
+/* eslint regexp/no-unused-capturing-group: ["error", { fixable: true }] */
 
 /* ✓ GOOD */
 var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2/$3') // "2000/12/31"
@@ -50,7 +52,20 @@ var index = '2000-12-31'.search(/(\d{4})-(\d{2})-(\d{2})/) // 0
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "regexp/no-unused-capturing-group": ["error", {
+    "fixable": true
+  }]
+}
+```
+
+- `fixable: true | false`
+
+  This option controls whether the rule is fixable. Defaults to `false`.
+
+  This rule is not fixable by default. Unused capturing groups can indicate a mistake in the code that uses the regex, so changing the regex might not be the right fix. When enabling this option, be sure to carefully check its changes.
+
 
 ## :couple: Related rules
 
diff --git a/lib/rules/negation.ts b/lib/rules/negation.ts
index 4fe008c..d1e616c 100644
--- a/lib/rules/negation.ts
+++ b/lib/rules/negation.ts
@@ -31,23 +31,51 @@ export default createRule("negation", {
             node,
             getRegexpLocation,
             fixReplaceNode,
+            toCharSet,
+            flags,
         }: RegExpContext): RegExpVisitor.Handlers {
             return {
                 onCharacterClassEnter(ccNode) {
                     if (!ccNode.negate || ccNode.elements.length !== 1) {
                         return
                     }
+
                     const element = ccNode.elements[0]
-                    if (element.type === "CharacterSet") {
-                        const negatedCharSet = getNegationText(element)
-                        context.report({
-                            node,
-                            loc: getRegexpLocation(ccNode),
-                            messageId: "unexpected",
-                            data: { negatedCharSet },
-                            fix: fixReplaceNode(ccNode, negatedCharSet),
+                    if (element.type !== "CharacterSet") {
+                        return
+                    }
+
+                    if (flags.ignoreCase && element.kind === "property") {
+                        // The ignore case canonicalization affects negated
+                        // Unicode property escapes in a weird way. In short,
+                        // /\p{Foo}/i is not the same as /[^\P{Foo}]/i if
+                        // \p{Foo} contains case-varying characters.
+                        //
+                        // Note: This only affects Unicode property escapes.
+                        // All other character sets are either case-invariant
+                        // (/./, /\s/, /\d/) or inconsistent (/\w/).
+
+                        const ccSet = toCharSet(ccNode)
+
+                        const negatedElementSet = toCharSet({
+                            ...element,
+                            negate: !element.negate,
                         })
+
+                        if (!ccSet.equals(negatedElementSet)) {
+                            // We cannot remove the negative
+                            return
+                        }
                     }
+
+                    const negatedCharSet = getNegationText(element)
+                    context.report({
+                        node,
+                        loc: getRegexpLocation(ccNode),
+                        messageId: "unexpected",
+                        data: { negatedCharSet },
+                        fix: fixReplaceNode(ccNode, negatedCharSet),
+                    })
                 },
             }
         }
diff --git a/lib/rules/no-unused-capturing-group.ts b/lib/rules/no-unused-capturing-group.ts
index b17e263..927b392 100644
--- a/lib/rules/no-unused-capturing-group.ts
+++ b/lib/rules/no-unused-capturing-group.ts
@@ -15,8 +15,10 @@ import {
     extractPropertyReferences,
 } from "../utils/ast-utils"
 import { createTypeTracker } from "../utils/type-tracker"
-import type { CapturingGroup } from "regexpp/ast"
+import type { CapturingGroup, Pattern } from "regexpp/ast"
 import { parseReplacementsForString } from "../utils/replacements-utils"
+import { getCapturingGroupNumber } from "regexp-ast-analysis"
+import { visitRegExpAST } from "regexpp"
 
 class CapturingData {
     private readonly unused = new Set<CapturingGroup>()
@@ -137,6 +139,35 @@ class CapturingData {
     }
 }
 
+/**
+ * Returns an identifier for the given capturing group.
+ *
+ * This is either the name of the group or its number.
+ */
+function getCapturingGroupIdentifier(group: CapturingGroup): string {
+    if (group.name) {
+        return `'${group.name}'`
+    }
+    return `number ${getCapturingGroupNumber(group)}`
+}
+
+/** Returns a list of all capturing groups in the order of their numbers. */
+function getAllCapturingGroups(pattern: Pattern): CapturingGroup[] {
+    const groups: CapturingGroup[] = []
+
+    visitRegExpAST(pattern, {
+        onCapturingGroupEnter(group) {
+            groups.push(group)
+        },
+    })
+
+    // visitRegExpAST given no guarantees in which order nodes are visited.
+    // Sort the list to guarantee order.
+    groups.sort((a, b) => a.start - b.start)
+
+    return groups
+}
+
 export default createRule("no-unused-capturing-group", {
     meta: {
         docs: {
@@ -144,17 +175,31 @@ export default createRule("no-unused-capturing-group", {
             category: "Best Practices",
             recommended: false,
         },
-        schema: [],
+        fixable: "code",
+        schema: [
+            {
+                type: "object",
+                properties: {
+                    fixable: { type: "boolean" },
+                },
+                additionalProperties: false,
+            },
+        ],
         messages: {
-            unusedCapturingGroup: "Capturing group is defined but never used.",
-            unusedNamedCapturingGroup:
-                "'{{name}}' capturing group is defined but never used.",
+            unusedCapturingGroup:
+                "Capturing group {{identifier}} is defined but never used.",
             unusedName:
-                "'{{name}}' is defined for capturing group, but it name is never used.",
+                "Capturing group {{identifier}} has a name, but its name is never used.",
+
+            // suggestions
+            makeNonCapturing: "Making this a non-capturing group.",
+            removeName: "Remove the unused name.",
         },
         type: "suggestion", // "problem",
     },
     create(context) {
+        const fixable: boolean = context.options[0]?.fixable ?? false
+
         const typeTracer = createTypeTracker(context)
         const capturingDataMap = new Map<Expression, CapturingData>()
 
@@ -162,27 +207,59 @@ export default createRule("no-unused-capturing-group", {
          * Report for unused
          */
         function reportUnused(capturingData: CapturingData) {
-            const { node, getRegexpLocation } = capturingData.regexpContext
+            const {
+                node,
+                getRegexpLocation,
+                fixReplaceNode,
+                patternAst,
+            } = capturingData.regexpContext
             const {
                 unusedCapturingGroups,
                 unusedNames,
             } = capturingData.getUnused()
+
+            const fixableGroups = new Set<CapturingGroup>()
+            for (const group of getAllCapturingGroups(patternAst).reverse()) {
+                if (unusedCapturingGroups.has(group)) {
+                    fixableGroups.add(group)
+                } else {
+                    break
+                }
+            }
+
             for (const cgNode of unusedCapturingGroups) {
+                const fix = fixableGroups.has(cgNode)
+                    ? fixReplaceNode(
+                          cgNode,
+                          cgNode.raw.replace(/^\((?:\?<[^<>]+>)?/, "(?:"),
+                      )
+                    : null
+
                 context.report({
                     node,
                     loc: getRegexpLocation(cgNode),
-                    messageId: cgNode.name
-                        ? "unusedNamedCapturingGroup"
-                        : "unusedCapturingGroup",
-                    data: cgNode.name ? { name: cgNode.name } : {},
+                    messageId: "unusedCapturingGroup",
+                    data: { identifier: getCapturingGroupIdentifier(cgNode) },
+                    fix: fixable ? fix : null,
+                    suggest: fix
+                        ? [{ messageId: "makeNonCapturing", fix }]
+                        : null,
                 })
             }
+
             for (const cgNode of unusedNames) {
+                const fix = fixReplaceNode(
+                    cgNode,
+                    cgNode.raw.replace(/^\(\?<[^<>]+>/, "("),
+                )
+
                 context.report({
                     node,
                     loc: getRegexpLocation(cgNode),
                     messageId: "unusedName",
-                    data: { name: cgNode.name },
+                    data: { identifier: getCapturingGroupIdentifier(cgNode) },
+                    fix: fixable ? fix : null,
+                    suggest: [{ messageId: "removeName", fix }],
                 })
             }
         }
diff --git a/package-lock.json b/package-lock.json
index f93affa..561bbfa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1370,9 +1370,9 @@
             "dev": true
         },
         "@types/mocha": {
-            "version": "8.2.2",
-            "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz",
-            "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==",
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz",
+            "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
             "dev": true
         },
         "@types/node": {
@@ -6324,15 +6324,34 @@
             }
         },
         "eslint-plugin-yml": {
-            "version": "0.9.0",
-            "resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-0.9.0.tgz",
-            "integrity": "sha512-0hHBm/aOyaRSQ3y7iEJ5uSIksYf2kdv3riMLEJoyyOBoQ6p1ODxJCBBhWdo/cbnFZRPvGhzDd5RCTqgExsIqMA==",
+            "version": "0.10.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-0.10.0.tgz",
+            "integrity": "sha512-N3F9Zcq9SeEphK02FdLWAg4eMbOzXTZ9K6QrPuv27mW212yOmEIrTiqzA29w2NBQGsfKMxfEQRV86JVfJmWJ8w==",
             "dev": true,
             "requires": {
                 "debug": "^4.1.1",
                 "lodash": "^4.17.19",
                 "natural-compare": "^1.4.0",
-                "yaml-eslint-parser": "^0.3.2"
+                "yaml-eslint-parser": "^0.4.0"
+            },
+            "dependencies": {
+                "eslint-visitor-keys": {
+                    "version": "2.1.0",
+                    "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+                    "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+                    "dev": true
+                },
+                "yaml-eslint-parser": {
+                    "version": "0.4.1",
+                    "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-0.4.1.tgz",
+                    "integrity": "sha512-GoJ/p1EW8O2tbTbuhfxjo1XhfUFU3uX3kwvfEQoOaZjO2Lubx8POjlsSqB+18b3SxkujAdQYT9r9nURaUWNYWQ==",
+                    "dev": true,
+                    "requires": {
+                        "eslint-visitor-keys": "^2.1.0",
+                        "lodash": "^4.17.20",
+                        "yaml": "^1.10.0"
+                    }
+                }
             }
         },
         "eslint-scope": {
diff --git a/package.json b/package.json
index d22671c..e0ac036 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
         "@types/eslint": "^7.2.0",
         "@types/eslint-scope": "^3.7.0",
         "@types/estree": "^0.0.50",
-        "@types/mocha": "^8.0.0",
+        "@types/mocha": "^9.0.0",
         "@types/node": "^14.14.39",
         "@typescript-eslint/eslint-plugin": "^4.14.0",
         "@typescript-eslint/parser": "^4.14.0",
@@ -72,7 +72,7 @@
         "eslint-plugin-prettier": "^3.4.0",
         "eslint-plugin-regexp": "^0.13.1",
         "eslint-plugin-vue": "^7.5.0",
-        "eslint-plugin-yml": "^0.9.0",
+        "eslint-plugin-yml": "^0.10.0",
         "eslint4b": "^7.3.1",
         "mocha": "^9.0.0",
         "nyc": "^15.1.0",


Test patch:
diff --git a/tests/lib/rules/negation.ts b/tests/lib/rules/negation.ts
index cdd1097..f84b386 100644
--- a/tests/lib/rules/negation.ts
+++ b/tests/lib/rules/negation.ts
@@ -9,7 +9,12 @@ const tester = new RuleTester({
 })
 
 tester.run("negation", rule as any, {
-    valid: [String.raw`/[\d]/`, String.raw`/[^\d\s]/`],
+    valid: [
+        String.raw`/[\d]/`,
+        String.raw`/[^\d\s]/`,
+        String.raw`/[^\p{ASCII}]/iu`,
+        String.raw`/[^\P{Ll}]/iu`,
+    ],
     invalid: [
         {
             code: String.raw`/[^\d]/`,
@@ -94,6 +99,13 @@ tester.run("negation", rule as any, {
                 "Unexpected negated character class. Use '\\p{Ll}' instead.",
             ],
         },
+        {
+            code: String.raw`/[^\P{White_Space}]/iu;`,
+            output: String.raw`/\p{White_Space}/iu;`,
+            errors: [
+                "Unexpected negated character class. Use '\\p{White_Space}' instead.",
+            ],
+        },
         {
             code: String.raw`const s ="[^\\w]"
             new RegExp(s)`,
diff --git a/tests/lib/rules/no-unused-capturing-group.ts b/tests/lib/rules/no-unused-capturing-group.ts
index 9bc8840..ebb9612 100644
--- a/tests/lib/rules/no-unused-capturing-group.ts
+++ b/tests/lib/rules/no-unused-capturing-group.ts
@@ -1,3 +1,5 @@
+// eslint-disable-next-line eslint-comments/disable-enable-pair -- x
+/* eslint-disable eslint-plugin/consistent-output -- x */
 import { RuleTester } from "eslint"
 import rule from "../../../lib/rules/no-unused-capturing-group"
 
@@ -26,14 +28,14 @@ tester.run("no-unused-capturing-group", rule as any, {
         String.raw`
         var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2/$3') // "2000/12/31"
         var replaced = '2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$<y>/$<m>/$<d>') // "2000/12/31"
-        
+
         var isDate = /(?:\d{4})-(?:\d{2})-(?:\d{2})/.test('2000-12-31') // true
-        
+
         var matches = '2000-12-31 2001-01-01'.match(/(\d{4})-(\d{2})-(\d{2})/)
         var y = matches[1] // "2000"
         var m = matches[2] // "12"
         var d = matches[3] // "31"
-        
+
         var index = '2000-12-31'.search(/(?:\d{4})-(?:\d{2})-(?:\d{2})/) // 0
         `,
         "var replaced = '2000-12-31'.replace(/(\\d{4})-(\\d{2})-(\\d{2})/, (_, y, m, d) => `${y}/${m}/${d}`)",
@@ -47,12 +49,12 @@ tester.run("no-unused-capturing-group", rule as any, {
         var replaced = '2000-12-31'.repl(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2')
 
         // var isDate = /(\d{4})-(\d{2})-(\d{2})/.test('2000-12-31')
-        
+
         // var matches = '2000-12-31 2001-01-01'.match(/(\d{4})-(\d{2})-(\d{2})/g)
         var matches = '2000-12-31 2001-01-01'.match(/\d{4}-\d{2}-\d{2}/g)
         var matches = '2000-12-31 2001-01-01'.match(unknown)
         var matches = (20001231).match(/(\d{4})-(\d{2})-(\d{2})/g)
-        
+
         // var index = '2000-12-31'.search(/(\d{4})-(\d{2})-(\d{2})/)
         var index = '2000-12-31'.search(/\d{4}-\d{2}-\d{2}/)
         var index = '2000-12-31'.search(unknown)
@@ -63,7 +65,7 @@ tester.run("no-unused-capturing-group", rule as any, {
         // String.raw`var replaced = '2000-12-31'.replace(/(?<n>\d{4})|(?<n>\d{2})|(?<n>\d{1})/, '$<n>')`,
         String.raw`
         const regexp = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/
-        let re 
+        let re
         while(re = regexp.exec('2000-12-31 2001-01-01')) {
             const y = re.groups.y // "2000"
             const m = re.groups.m // "12"
@@ -115,163 +117,73 @@ tester.run("no-unused-capturing-group", rule as any, {
     ],
     invalid: [
         {
-            code: String.raw`
-            var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, 'Date') // "Date"
-            var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2') // "2000/12"
-            var replaced = '2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$<y>/$<m>') // "2000/12"
-            var replaced = '2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$1/$2/$3') // "2000/12/31"
-            
-            var isDate = /(\d{4})-(\d{2})-(\d{2})/.test('2000-12-31') // true
-            
-            var matches = '2000-12-31 2001-01-01'.match(/(\d{4})-(\d{2})-(\d{2})/g) // ["2000-12-31", "2001-01-01"]
-            
-            var index = '2000-12-31'.search(/(\d{4})-(\d{2})-(\d{2})/) // 0
-            `,
+            code: String.raw`'2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, 'Date') `,
+            output: String.raw`'2000-12-31'.replace(/(?:\d{4})-(?:\d{2})-(?:\d{2})/, 'Date') `,
+            options: [{ fixable: true }],
             errors: [
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 2,
-                    column: 50,
-                    endLine: 2,
-                    endColumn: 57,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 2,
-                    column: 58,
-                    endLine: 2,
-                    endColumn: 65,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 2,
-                    column: 66,
-                    endLine: 2,
-                    endColumn: 73,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 3,
-                    column: 66,
-                    endLine: 3,
-                    endColumn: 73,
-                },
-                {
-                    message: "'d' capturing group is defined but never used.",
-                    line: 4,
-                    column: 74,
-                    endLine: 4,
-                    endColumn: 85,
-                },
-                {
-                    message:
-                        "'y' is defined for capturing group, but it name is never used.",
-                    line: 5,
-                    column: 50,
-                    endLine: 5,
-                    endColumn: 61,
-                },
-                {
-                    message:
-                        "'m' is defined for capturing group, but it name is never used.",
-                    line: 5,
-                    column: 62,
-                    endLine: 5,
-                    endColumn: 73,
-                },
-                {
-                    message:
-                        "'d' is defined for capturing group, but it name is never used.",
-                    line: 5,
-                    column: 74,
-                    endLine: 5,
-                    endColumn: 85,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 7,
-                    column: 27,
-                    endLine: 7,
-                    endColumn: 34,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 7,
-                    column: 35,
-                    endLine: 7,
-                    endColumn: 42,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 7,
-                    column: 43,
-                    endLine: 7,
-                    endColumn: 50,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 9,
-                    column: 58,
-                    endLine: 9,
-                    endColumn: 65,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 9,
-                    column: 66,
-                    endLine: 9,
-                    endColumn: 73,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 9,
-                    column: 74,
-                    endLine: 9,
-                    endColumn: 81,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 11,
-                    column: 46,
-                    endLine: 11,
-                    endColumn: 53,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 11,
-                    column: 54,
-                    endLine: 11,
-                    endColumn: 61,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 11,
-                    column: 62,
-                    endLine: 11,
-                    endColumn: 69,
-                },
+                "Capturing group number 1 is defined but never used.",
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
             ],
         },
         {
-            code:
-                "var replaced = '2000-12-31'.replace(/(\\d{4})-(\\d{2})-(\\d{2})/, (_, y, m) => `${y}/${m}`)",
+            code: String.raw`'2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2') // "2000/12"`,
+            output: String.raw`'2000-12-31'.replace(/(\d{4})-(\d{2})-(?:\d{2})/, '$1/$2') // "2000/12"`,
+            options: [{ fixable: true }],
+            errors: ["Capturing group number 3 is defined but never used."],
+        },
+        {
+            code: String.raw`'2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$<y>/$<m>') // "2000/12"`,
+            output: String.raw`'2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?:\d{2})/u, '$<y>/$<m>') // "2000/12"`,
+            options: [{ fixable: true }],
+            errors: ["Capturing group 'd' is defined but never used."],
+        },
+        {
+            code: String.raw`'2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$1/$2/$3') // "2000/12/31"`,
+            output: String.raw`'2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/u, '$1/$2/$3') // "2000/12/31"`,
+            options: [{ fixable: true }],
             errors: [
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 1,
-                    column: 54,
-                    endLine: 1,
-                    endColumn: 61,
-                },
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group 'm' has a name, but its name is never used.",
+                "Capturing group 'd' has a name, but its name is never used.",
             ],
         },
+        {
+            code: String.raw`/(\d{4})-(\d{2})-(\d{2})/.test('2000-12-31') // true`,
+            errors: [
+                "Capturing group number 1 is defined but never used.",
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
+            ],
+        },
+        {
+            code: String.raw`'2000-12-31 2001-01-01'.match(/(\d{4})-(\d{2})-(\d{2})/g) // ["2000-12-31", "2001-01-01"]`,
+            errors: [
+                "Capturing group number 1 is defined but never used.",
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
+            ],
+        },
+        {
+            code: String.raw`'2000-12-31'.search(/(\d{4})-(\d{2})-(\d{2})/) // 0`,
+            errors: [
+                "Capturing group number 1 is defined but never used.",
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
+            ],
+        },
+        {
+            code:
+                "var replaced = '2000-12-31'.replace(/(\\d{4})-(\\d{2})-(\\d{2})/, (_, y, m) => `${y}/${m}`)",
+            errors: ["Capturing group number 3 is defined but never used."],
+        },
         {
             code:
                 "var replaced = '2000-12-31'.replace(/(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})/, (_, y, m, d) => `${y}/${m}/${d}`)",
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "'m' is defined for capturing group, but it name is never used.",
-                "'d' is defined for capturing group, but it name is never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group 'm' has a name, but its name is never used.",
+                "Capturing group 'd' has a name, but its name is never used.",
             ],
         },
         {
@@ -280,12 +192,12 @@ tester.run("no-unused-capturing-group", rule as any, {
             var replaced = '2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$<yy>/$<mm>/$<dd>')
             `,
             errors: [
-                "Capturing group is defined but never used.",
-                "Capturing group is defined but never used.",
-                "Capturing group is defined but never used.",
-                "'y' capturing group is defined but never used.",
-                "'m' capturing group is defined but never used.",
-                "'d' capturing group is defined but never used.",
+                "Capturing group number 1 is defined but never used.",
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
+                "Capturing group 'y' is defined but never used.",
+                "Capturing group 'm' is defined but never used.",
+                "Capturing group 'd' is defined but never used.",
             ],
         },
         {
@@ -293,16 +205,17 @@ tester.run("no-unused-capturing-group", rule as any, {
             var replaced = '2000-12-31'.replaceAll(/(\d{4})-(\d{2})-(\d{2})/g, 'foo')
             `,
             errors: [
-                "Capturing group is defined but never used.",
-                "Capturing group is defined but never used.",
-                "Capturing group is defined but never used.",
+                "Capturing group number 1 is defined but never used.",
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
             ],
         },
         {
             code: String.raw`var index = '2000-12-31 2000-12-31'.search(/(\d{4})-(\d{2})-(\d{2}) \1-\2-\2/)`,
             errors: [
                 {
-                    message: "Capturing group is defined but never used.",
+                    message:
+                        "Capturing group number 3 is defined but never used.",
                     line: 1,
                     column: 61,
                     endLine: 1,
@@ -312,15 +225,7 @@ tester.run("no-unused-capturing-group", rule as any, {
         },
         {
             code: String.raw`var index = '2000-12-31 2000-12-31'.search(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2}) \k<y>-\k<d>-\k<d>/)`,
-            errors: [
-                {
-                    message: "'m' capturing group is defined but never used.",
-                    line: 1,
-                    column: 57,
-                    endLine: 1,
-                    endColumn: 68,
-                },
-            ],
+            errors: ["Capturing group 'm' is defined but never used."],
         },
         {
             code: String.raw`
@@ -329,15 +234,7 @@ tester.run("no-unused-capturing-group", rule as any, {
             var m = matches[2] // "12"
             // var d = matches[3] // "31"
             `,
-            errors: [
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 2,
-                    column: 63,
-                    endLine: 2,
-                    endColumn: 70,
-                },
-            ],
+            errors: ["Capturing group number 3 is defined but never used."],
         },
         {
             code: String.raw`
@@ -345,7 +242,7 @@ tester.run("no-unused-capturing-group", rule as any, {
             const y = groups.y // "2000"
             const d = groups.d // "31"
             `,
-            errors: ["'m' capturing group is defined but never used."],
+            errors: ["Capturing group 'm' is defined but never used."],
         },
         {
             code: String.raw`
@@ -353,41 +250,41 @@ tester.run("no-unused-capturing-group", rule as any, {
             const y = groups.y // "2000"
             const d = groups.d // "31"
             `,
-            errors: ["'m' capturing group is defined but never used."],
+            errors: ["Capturing group 'm' is defined but never used."],
         },
         {
             code: String.raw`
             const { groups: {y,d} } = '2000-12-31'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)
             `,
-            errors: ["'m' capturing group is defined but never used."],
+            errors: ["Capturing group 'm' is defined but never used."],
         },
         {
             code: String.raw`
             const [,y,m,d] = '2000-12-31'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "'m' is defined for capturing group, but it name is never used.",
-                "'d' is defined for capturing group, but it name is never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group 'm' has a name, but its name is never used.",
+                "Capturing group 'd' has a name, but its name is never used.",
             ],
         },
         {
             code: String.raw`
             const {y,m} = '2000-12-31'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)["groups"]
             `,
-            errors: ["'d' capturing group is defined but never used."],
+            errors: ["Capturing group 'd' is defined but never used."],
         },
         {
             code: String.raw`
             const { groups: {m, d} = {} } = '2000-12-31'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)
             `,
-            errors: ["'y' capturing group is defined but never used."],
+            errors: ["Capturing group 'y' is defined but never used."],
         },
         {
             code: String.raw`
             const { ["groups"]: {m, d} = {} } = '2000-12-31'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)
             `,
-            errors: ["'y' capturing group is defined but never used."],
+            errors: ["Capturing group 'y' is defined but never used."],
         },
         {
             code: String.raw`
@@ -396,23 +293,11 @@ tester.run("no-unused-capturing-group", rule as any, {
                 // var m = matches[2] // "12"
                 // var d = matches[3] // "31"
             }
-            
+
             `,
             errors: [
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 2,
-                    column: 77,
-                    endLine: 2,
-                    endColumn: 84,
-                },
-                {
-                    message: "Capturing group is defined but never used.",
-                    line: 2,
-                    column: 85,
-                    endLine: 2,
-                    endColumn: 92,
-                },
+                "Capturing group number 2 is defined but never used.",
+                "Capturing group number 3 is defined but never used.",
             ],
         },
         {
@@ -425,8 +310,8 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "'d' capturing group is defined but never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group 'd' is defined but never used.",
             ],
         },
         {
@@ -438,11 +323,11 @@ tester.run("no-unused-capturing-group", rule as any, {
                 var m = matches.groups.m // "12"
                 // var d = matches[3] // "31"
             }
-            
+
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "'d' capturing group is defined but never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group 'd' is defined but never used.",
             ],
         },
         {
@@ -457,8 +342,8 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "Capturing group is defined but never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group number 3 is defined but never used.",
             ],
         },
         {
@@ -471,8 +356,8 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "Capturing group is defined but never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group number 3 is defined but never used.",
             ],
         },
         {
@@ -489,10 +374,10 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "Capturing group is defined but never used.",
-                "'y' is defined for capturing group, but it name is never used.",
-                "'d' capturing group is defined but never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group number 3 is defined but never used.",
+                "Capturing group 'y' has a name, but its name is never used.",
+                "Capturing group 'd' is defined but never used.",
             ],
         },
         {
@@ -506,8 +391,42 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                "'y' is defined for capturing group, but it name is never used.",
-                "Capturing group is defined but never used.",
+                {
+                    message:
+                        "Capturing group 'y' has a name, but its name is never used.",
+                    suggestions: [
+                        {
+                            messageId: "removeName",
+                            output: String.raw`
+            const result = [...'2000-12-31 2000-12-31'.matchAll(/(\d{4})-(?<m>\d{2})-(\d{2})/g)]
+            for (let index = 0; index < result.length; index++) {
+                const matches = result[index]
+                var y = matches[1] // "2000"
+                var m = matches.groups.m // "12"
+                // var d = matches[3] // "31"
+            }
+            `,
+                        },
+                    ],
+                },
+                {
+                    message:
+                        "Capturing group number 3 is defined but never used.",
+                    suggestions: [
+                        {
+                            messageId: "makeNonCapturing",
+                            output: String.raw`
+            const result = [...'2000-12-31 2000-12-31'.matchAll(/(?<y>\d{4})-(?<m>\d{2})-(?:\d{2})/g)]
+            for (let index = 0; index < result.length; index++) {
+                const matches = result[index]
+                var y = matches[1] // "2000"
+                var m = matches.groups.m // "12"
+                // var d = matches[3] // "31"
+            }
+            `,
+                        },
+                    ],
+                },
             ],
         },
         {
@@ -516,38 +435,99 @@ tester.run("no-unused-capturing-group", rule as any, {
             'a,b,c'.split(reg)
             `,
             errors: [
-                "'comma' is defined for capturing group, but it name is never used.",
+                {
+                    message:
+                        "Capturing group 'comma' has a name, but its name is never used.",
+                    suggestions: [
+                        {
+                            messageId: "removeName",
+                            output: String.raw`
+            const reg = /(,)/;
+            'a,b,c'.split(reg)
+            `,
+                        },
+                    ],
+                },
             ],
         },
         {
             code: String.raw`var isDate = /(\d{4})-(\d{2})-(\d{2})/.test(foo)`,
             errors: [
-                "Capturing group is defined but never used.",
-                "Capturing group is defined but never used.",
-                "Capturing group is defined but never used.",
+                {
+                    message:
+                        "Capturing group number 1 is defined but never used.",
+                    suggestions: [
+                        {
+                            messageId: "makeNonCapturing",
+                            output: String.raw`var isDate = /(?:\d{4})-(\d{2})-(\d{2})/.test(foo)`,
+                        },
+                    ],
+                },
+                {
+                    message:
+                        "Capturing group number 2 is defined but never used.",
+                    suggestions: [
+                        {
+                            messageId: "makeNonCapturing",
+                            output: String.raw`var isDate = /(\d{4})-(?:\d{2})-(\d{2})/.test(foo)`,
+                        },
+                    ],
+                },
+                {
+                    message:
+                        "Capturing group number 3 is defined but never used.",
+                    suggestions: [
+                        {
+                            messageId: "makeNonCapturing",
+                            output: String.raw`var isDate = /(\d{4})-(\d{2})-(?:\d{2})/.test(foo)`,
+                        },
+                    ],
+                },
             ],
         },
         {
             code: String.raw`
             const regexp = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/
-            let re 
+            let re
             while(re = regexp.exec(foo)) {
                 const y = re.groups.y
                 // const m = re.groups.m
                 const d = re.groups.d
             }
             `,
-            errors: ["'m' capturing group is defined but never used."],
+            errors: [
+                {
+                    message: "Capturing group 'm' is defined but never used.",
+                    suggestions: [],
+                },
+            ],
         },
         {
             code: String.raw`
             const regexp = /(\d{4})-(\d{2})-(\d{2})/
-            let re 
+            let re
             while(re = regexp.exec(foo)) {
                 const [,y,m] = re
             }
             `,
-            errors: ["Capturing group is defined but never used."],
+            errors: [
+                {
+                    message:
+                        "Capturing group number 3 is defined but never used.",
+                    suggestions: [
+                        {
+                            messageId: "makeNonCapturing",
+                            output: String.raw`
+            const regexp = /(\d{4})-(\d{2})-(?:\d{2})/
+            let re
+            while(re = regexp.exec(foo)) {
+                const [,y,m] = re
+            }
+            `,
+                        },
+                    ],
+                },
+            ],
         },
     ],
 })