Repository: ota-meshi/eslint-plugin-regexp
Pull request #349 (2021-10-01 15:14:38)

Remove the unused name check from `regexp/no-unused-capturing-group` rule
This PR removes unused name checks from the `regexp/no-unused-capturing-group` rule.

Remove the unused name check because it was a check that could not be inferred from the name of this rule and there was a conflict with the `prefer-named-backreference` rule.

See https://github.com/ota-meshi/eslint-plugin-regexp/pull/339#discussion_r720309918

We also plan to add rules that prefers using the names of named capturing groups defined in regex. #344, #347.

---

This PR includes a refactoring to move the capture group usage to common function.
This will also be used in the new rule #344 and #347.





Patch:
diff --git a/docs/rules/no-unused-capturing-group.md b/docs/rules/no-unused-capturing-group.md
index e322e79..9400dc8 100644
--- a/docs/rules/no-unused-capturing-group.md
+++ b/docs/rules/no-unused-capturing-group.md
@@ -39,7 +39,6 @@ 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})/, '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 replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, (_, y, m) => `${y}/${m}`) // "2000/12"
 
 var isDate = /(\d{4})-(\d{2})-(\d{2})/.test('2000-12-31') // true
@@ -76,7 +75,7 @@ Using capturing groups only if the captured text is used makes their usage unamb
 ```json
 {
   "regexp/no-unused-capturing-group": ["error", {
-    "fixable": true
+    "fixable": false
   }]
 }
 ```
@@ -87,10 +86,11 @@ Using capturing groups only if the captured text is used makes their usage unamb
 
   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
 
-- [regexp/no-useless-dollar-replacements](./no-useless-dollar-replacements.md)
+- [regexp/no-useless-dollar-replacements]
+
+[regexp/no-useless-dollar-replacements]: ./no-useless-dollar-replacements.md
 
 ## :rocket: Version
 
diff --git a/lib/rules/no-unused-capturing-group.ts b/lib/rules/no-unused-capturing-group.ts
index d39c07f..25e1bde 100644
--- a/lib/rules/no-unused-capturing-group.ts
+++ b/lib/rules/no-unused-capturing-group.ts
@@ -1,143 +1,8 @@
-import type {
-    ArrowFunctionExpression,
-    CallExpression,
-    Expression,
-    FunctionExpression,
-} from "estree"
 import type { RegExpVisitor } from "regexpp/visitor"
 import type { RegExpContext } from "../utils"
-import { createRule, defineRegexpVisitor, compositingVisitors } from "../utils"
-import type { KnownMethodCall, PropertyReference } from "../utils/ast-utils"
-import {
-    isKnownMethodCall,
-    getStaticValue,
-    extractExpressionReferences,
-    extractPropertyReferences,
-} from "../utils/ast-utils"
-import { createTypeTracker } from "../utils/type-tracker"
-import type { CapturingGroup, Pattern } from "regexpp/ast"
-import { parseReplacementsForString } from "../utils/replacements-utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import type { CapturingGroup } from "regexpp/ast"
 import { getCapturingGroupNumber } from "regexp-ast-analysis"
-import { visitRegExpAST } from "regexpp"
-
-class CapturingData {
-    private readonly unused = new Set<CapturingGroup>()
-
-    private readonly unusedNames = new Map<string, CapturingGroup[]>()
-
-    private readonly indexToCapturingGroup = new Map<number, CapturingGroup>()
-
-    public countOfCapturingGroup = 0
-
-    public readonly regexpContext: RegExpContext
-
-    private readonly state = {
-        used: false,
-        track: true,
-    }
-
-    public constructor(regexpContext: RegExpContext) {
-        this.regexpContext = regexpContext
-    }
-
-    public getUnused(): {
-        unusedCapturingGroups: Set<CapturingGroup>
-        unusedNames: Set<CapturingGroup & { name: string }>
-    } {
-        const unusedCapturingGroups = new Set(this.unused)
-        const unusedNames = new Set<CapturingGroup & { name: string }>()
-
-        for (const cgNodes of this.unusedNames.values()) {
-            for (const cgNode of cgNodes) {
-                if (!unusedCapturingGroups.has(cgNode)) {
-                    unusedNames.add(cgNode as never)
-                }
-            }
-        }
-        return {
-            unusedCapturingGroups,
-            unusedNames,
-        }
-    }
-
-    public usedIndex(ref: number) {
-        const cgNode = this.indexToCapturingGroup.get(ref)
-        if (cgNode) {
-            this.unused.delete(cgNode)
-        }
-    }
-
-    public usedName(ref: string) {
-        const cgNodes = this.unusedNames.get(ref)
-        if (cgNodes) {
-            this.unusedNames.delete(ref)
-            for (const cgNode of cgNodes) {
-                this.unused.delete(cgNode)
-            }
-        }
-    }
-
-    public usedAllNames() {
-        for (const cgNodes of this.unusedNames.values()) {
-            for (const cgNode of cgNodes) {
-                this.unused.delete(cgNode)
-            }
-        }
-        this.unusedNames.clear()
-    }
-
-    public usedAllUnnamed() {
-        this.unused.clear()
-    }
-
-    public isAllUsed() {
-        return !this.unused.size && !this.unusedNames.size
-    }
-
-    public markAsUsed() {
-        this.state.used = true
-    }
-
-    public markAsCannotTrack() {
-        this.state.track = false
-    }
-
-    public isNeedReport() {
-        return this.state.used && this.state.track && !this.isAllUsed()
-    }
-
-    public visitor(): RegExpVisitor.Handlers {
-        return {
-            onCapturingGroupEnter: (cgNode) => {
-                this.countOfCapturingGroup++
-
-                if (!cgNode.references.length) {
-                    this.unused.add(cgNode)
-                    this.indexToCapturingGroup.set(
-                        this.countOfCapturingGroup,
-                        cgNode,
-                    )
-                }
-                // used as reference
-                else if (
-                    cgNode.references.some((ref) => typeof ref.ref === "string")
-                ) {
-                    // reference name is used
-                    return
-                }
-
-                if (cgNode.name) {
-                    const array = this.unusedNames.get(cgNode.name)
-                    if (array) {
-                        array.push(cgNode)
-                    } else {
-                        this.unusedNames.set(cgNode.name, [cgNode])
-                    }
-                }
-            },
-        }
-    }
-}
 
 /**
  * Returns an identifier for the given capturing group.
@@ -151,23 +16,6 @@ function getCapturingGroupIdentifier(group: CapturingGroup): string {
     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: {
@@ -188,12 +36,9 @@ export default createRule("no-unused-capturing-group", {
         messages: {
             unusedCapturingGroup:
                 "Capturing group {{identifier}} is defined but never used.",
-            unusedName:
-                "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",
         hasSuggestions: true,
@@ -201,34 +46,30 @@ export default createRule("no-unused-capturing-group", {
     create(context) {
         const fixable: boolean = context.options[0]?.fixable ?? false
 
-        const typeTracer = createTypeTracker(context)
-        const capturingDataMap = new Map<Expression, CapturingData>()
-
         /**
          * Report for unused
          */
-        function reportUnused(capturingData: CapturingData) {
+        function reportUnused(
+            unused: Set<CapturingGroup>,
+            regexpContext: RegExpContext,
+        ) {
             const {
                 node,
                 getRegexpLocation,
                 fixReplaceNode,
-                patternAst,
-            } = capturingData.regexpContext
-            const {
-                unusedCapturingGroups,
-                unusedNames,
-            } = capturingData.getUnused()
+                getAllCapturingGroups,
+            } = regexpContext
 
             const fixableGroups = new Set<CapturingGroup>()
-            for (const group of getAllCapturingGroups(patternAst).reverse()) {
-                if (unusedCapturingGroups.has(group)) {
+            for (const group of [...getAllCapturingGroups()].reverse()) {
+                if (unused.has(group)) {
                     fixableGroups.add(group)
                 } else {
                     break
                 }
             }
 
-            for (const cgNode of unusedCapturingGroups) {
+            for (const cgNode of unused) {
                 const fix = fixableGroups.has(cgNode)
                     ? fixReplaceNode(
                           cgNode,
@@ -247,234 +88,63 @@ export default createRule("no-unused-capturing-group", {
                         : null,
                 })
             }
-
-            for (const cgNode of unusedNames) {
-                const fix = fixReplaceNode(
-                    cgNode,
-                    cgNode.raw.replace(/^\(\?<[^<>]+>/, "("),
-                )
-
-                context.report({
-                    node,
-                    loc: getRegexpLocation(cgNode),
-                    messageId: "unusedName",
-                    data: { identifier: getCapturingGroupIdentifier(cgNode) },
-                    fix: fixable ? fix : null,
-                    suggest: [{ messageId: "removeName", fix }],
-                })
-            }
-        }
-
-        /** Verify for String.prototype.match() */
-        function verifyForMatch(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.arguments[0])
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            if (!typeTracer.isString(node.callee.object)) {
-                capturingData.markAsCannotTrack()
-                return
-            }
-            if (capturingData.regexpContext.flags.global) {
-                // String.prototype.match() with g flag
-                capturingData.markAsUsed()
-            } else {
-                capturingData.markAsUsed()
-                verifyForExecResult(node, capturingData)
-            }
-        }
-
-        /** Verify for String.prototype.search() */
-        function verifyForSearch(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.arguments[0])
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            if (!typeTracer.isString(node.callee.object)) {
-                capturingData.markAsCannotTrack()
-                return
-            }
-            // String.prototype.search()
-            capturingData.markAsUsed()
-        }
-
-        /** Verify for RegExp.prototype.test() */
-        function verifyForTest(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.callee.object)
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            // RegExp.prototype.test()
-            capturingData.markAsUsed()
-        }
-
-        /** Verify for String.prototype.replace() and String.prototype.replaceAll() */
-        function verifyForReplace(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.arguments[0])
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            if (!typeTracer.isString(node.callee.object)) {
-                capturingData.markAsCannotTrack()
-                return
-            }
-            const replacementNode = node.arguments[1]
-            if (
-                replacementNode.type === "FunctionExpression" ||
-                replacementNode.type === "ArrowFunctionExpression"
-            ) {
-                capturingData.markAsUsed()
-                verifyForReplaceFunction(replacementNode, capturingData)
-            } else {
-                const evaluated = getStaticValue(context, node.arguments[1])
-                if (!evaluated || typeof evaluated.value !== "string") {
-                    capturingData.markAsCannotTrack()
-                    return
-                }
-                capturingData.markAsUsed()
-                verifyForReplacePattern(evaluated.value, capturingData)
-            }
-        }
-
-        /** Verify for String.prototype.replace(regexp, pattern) and String.prototype.replaceAll(regexp, pattern) */
-        function verifyForReplacePattern(
-            replacementPattern: string,
-            capturingData: CapturingData,
-        ) {
-            for (const replacement of parseReplacementsForString(
-                replacementPattern,
-            )) {
-                if (replacement.type === "ReferenceElement") {
-                    if (typeof replacement.ref === "number") {
-                        capturingData.usedIndex(replacement.ref)
-                    } else {
-                        capturingData.usedName(replacement.ref)
-                    }
-                }
-            }
         }
 
-        /** Verify for String.prototype.replace(regexp, fn) and String.prototype.replaceAll(regexp, fn) */
-        function verifyForReplaceFunction(
-            replacementNode: FunctionExpression | ArrowFunctionExpression,
-            capturingData: CapturingData,
-        ) {
-            for (
-                let index = 0;
-                index < replacementNode.params.length;
-                index++
-            ) {
-                const arg = replacementNode.params[index]
-                if (arg.type === "RestElement") {
-                    capturingData.markAsCannotTrack()
-                    return
-                }
-                if (index === 0) {
-                    continue
-                } else if (index <= capturingData.countOfCapturingGroup) {
-                    capturingData.usedIndex(index)
-                } else if (capturingData.countOfCapturingGroup + 3 <= index) {
-                    // used as name
-                    capturingData.usedAllNames()
+        /**
+         * Get all capturing group references
+         */
+        function getCapturingGroupReferences(regexpContext: RegExpContext) {
+            const capturingGroupReferences = regexpContext.getCapturingGroupReferences()
+            if (!capturingGroupReferences.length) {
+                // unused regexp
+                return null
+            }
+            const indexRefs: number[] = []
+            const namedRefs: string[] = []
+            let hasUnknownName = false
+            let hasSplit = false
+            for (const ref of capturingGroupReferences) {
+                if (ref.type === "UnknownUsage" || ref.type === "UnknownRef") {
+                    return null
                 }
-            }
-        }
-
-        /** Verify for RegExp.prototype.exec() */
-        function verifyForExec(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.callee.object)
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            capturingData.markAsUsed()
-            verifyForExecResult(node, capturingData)
-        }
-
-        /** Verify for RegExp.prototype.exec() and String.prototype.match() result */
-        function verifyForExecResult(
-            node: KnownMethodCall,
-            capturingData: CapturingData,
-        ) {
-            for (const ref of extractPropertyReferences(node, context)) {
-                if (hasNameRef(ref)) {
-                    if (ref.name === "groups") {
-                        for (const namedRef of ref.extractPropertyReferences()) {
-                            if (hasNameRef(namedRef)) {
-                                capturingData.usedName(namedRef.name)
-                            } else {
-                                // unknown used as name
-                                capturingData.usedAllNames()
-                            }
+                if (
+                    ref.type === "ArrayRef" ||
+                    ref.type === "ReplacementRef" ||
+                    ref.type === "ReplacerFunctionRef"
+                ) {
+                    if (ref.kind === "index") {
+                        if (ref.ref) {
+                            indexRefs.push(ref.ref)
+                        } else {
+                            return null
                         }
                     } else {
-                        capturingData.usedIndex(Number(ref.name))
-                    }
-                } else {
-                    capturingData.markAsCannotTrack()
-                    return
-                }
-            }
-        }
-
-        /** Verify for String.prototype.matchAll() */
-        function verifyForMatchAll(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.arguments[0])
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            if (!typeTracer.isString(node.callee.object)) {
-                capturingData.markAsCannotTrack()
-                return
-            }
-            capturingData.markAsUsed()
-            for (const iterationRef of extractPropertyReferences(
-                node,
-                context,
-            )) {
-                if (!iterationRef.extractPropertyReferences) {
-                    capturingData.markAsCannotTrack()
-                    return
-                }
-                if (hasNameRef(iterationRef)) {
-                    if (Number.isNaN(Number(iterationRef.name))) {
-                        // Not aimed to iteration.
-                        continue
-                    }
-                }
-                for (const ref of iterationRef.extractPropertyReferences()) {
-                    if (hasNameRef(ref)) {
-                        if (ref.name === "groups") {
-                            for (const namedRef of ref.extractPropertyReferences()) {
-                                if (hasNameRef(namedRef)) {
-                                    capturingData.usedName(namedRef.name)
-                                } else {
-                                    // unknown used as name
-                                    capturingData.usedAllNames()
-                                }
-                            }
+                        // named
+                        if (ref.ref) {
+                            namedRefs.push(ref.ref)
                         } else {
-                            capturingData.usedIndex(Number(ref.name))
+                            hasUnknownName = true
                         }
-                    } else {
-                        capturingData.markAsCannotTrack()
-                        return
                     }
+                } else if (ref.type === "Split") {
+                    hasSplit = true
                 }
             }
-        }
 
-        /** Verify for String.prototype.split() */
-        function verifyForSplit(node: KnownMethodCall) {
-            const capturingData = capturingDataMap.get(node.arguments[0])
-            if (capturingData == null || capturingData.isAllUsed()) {
-                return
-            }
-            if (!typeTracer.isString(node.callee.object)) {
-                capturingData.markAsCannotTrack()
-                return
+            return {
+                unusedIndexRef(index: number): boolean {
+                    if (hasSplit) {
+                        return false
+                    }
+                    return !indexRefs.includes(index)
+                },
+                unusedNamedRef(name: string): boolean {
+                    if (hasUnknownName) {
+                        return false
+                    }
+                    return !namedRefs.includes(name)
+                },
             }
-            capturingData.markAsUsed()
-            capturingData.usedAllUnnamed()
         }
 
         /**
@@ -483,78 +153,34 @@ export default createRule("no-unused-capturing-group", {
         function createVisitor(
             regexpContext: RegExpContext,
         ): RegExpVisitor.Handlers {
-            const { regexpNode } = regexpContext
-            const capturingData = new CapturingData(regexpContext)
-            capturingDataMap.set(regexpNode, capturingData)
-            for (const ref of extractExpressionReferences(
-                regexpNode,
-                context,
-            )) {
-                if (ref.type === "argument" || ref.type === "member") {
-                    capturingDataMap.set(ref.node, capturingData)
-                } else {
-                    capturingData.markAsCannotTrack()
+            const references = getCapturingGroupReferences(regexpContext)
+            if (!references) {
+                // unused regexp or unknown reference
+                return {}
+            }
+            const unused = new Set<CapturingGroup>()
+            const allCapturingGroups = regexpContext.getAllCapturingGroups()
+            for (let index = 0; index < allCapturingGroups.length; index++) {
+                const cgNode = allCapturingGroups[index]
+                if (
+                    cgNode.references.length ||
+                    !references.unusedIndexRef(index + 1)
+                ) {
+                    continue
                 }
+                if (cgNode.name && !references.unusedNamedRef(cgNode.name)) {
+                    continue
+                }
+                unused.add(cgNode)
             }
-            return capturingData.visitor()
+
+            reportUnused(unused, regexpContext)
+
+            return {}
         }
 
-        return compositingVisitors(
-            defineRegexpVisitor(context, {
-                createVisitor,
-            }),
-            {
-                "Program:exit"() {
-                    for (const capturingData of new Set(
-                        capturingDataMap.values(),
-                    )) {
-                        if (capturingData.isNeedReport()) {
-                            reportUnused(capturingData)
-                        }
-                    }
-                },
-                "CallExpression:exit"(node: CallExpression) {
-                    if (
-                        !isKnownMethodCall(node, {
-                            match: 1,
-                            test: 1,
-                            search: 1,
-                            replace: 2,
-                            replaceAll: 2,
-                            matchAll: 1,
-                            exec: 1,
-                            split: 1,
-                        })
-                    ) {
-                        return
-                    }
-                    if (node.callee.property.name === "match") {
-                        verifyForMatch(node)
-                    } else if (node.callee.property.name === "test") {
-                        verifyForTest(node)
-                    } else if (node.callee.property.name === "search") {
-                        verifyForSearch(node)
-                    } else if (
-                        node.callee.property.name === "replace" ||
-                        node.callee.property.name === "replaceAll"
-                    ) {
-                        verifyForReplace(node)
-                    } else if (node.callee.property.name === "exec") {
-                        verifyForExec(node)
-                    } else if (node.callee.property.name === "matchAll") {
-                        verifyForMatchAll(node)
-                    } else if (node.callee.property.name === "split") {
-                        verifyForSplit(node)
-                    }
-                },
-            },
-        )
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
     },
 })
-
-/** Checks whether the given reference is a named reference. */
-function hasNameRef(
-    ref: PropertyReference,
-): ref is PropertyReference & { type: "member" | "destructuring" } {
-    return ref.type === "destructuring" || ref.type === "member"
-}
diff --git a/lib/utils/ast-utils/extract-property-references.ts b/lib/utils/ast-utils/extract-property-references.ts
index 2486ed8..fd2ae5e 100644
--- a/lib/utils/ast-utils/extract-property-references.ts
+++ b/lib/utils/ast-utils/extract-property-references.ts
@@ -1,9 +1,10 @@
 import type { Rule } from "eslint"
 import type {
+    ForOfStatement,
     ArrayExpression,
     ArrayPattern,
+    AssignmentProperty,
     Expression,
-    ForOfStatement,
     MemberExpression,
     ObjectExpression,
     ObjectPattern,
@@ -21,20 +22,24 @@ export type PropertyReference =
     | {
           // Property name is unknown.
           type: "unknown"
+          node: Pattern | AssignmentProperty | Expression | ForOfStatement
           extractPropertyReferences?: () => Iterable<PropertyReference>
       }
     | {
           type: "member"
           name: string
+          node: MemberExpression
           extractPropertyReferences: () => Iterable<PropertyReference>
       }
     | {
           type: "destructuring"
           name: string
+          node: AssignmentProperty | Pattern
           extractPropertyReferences: () => Iterable<PropertyReference>
       }
     | {
           type: "iteration"
+          node: ForOfStatement
           extractPropertyReferences: () => Iterable<PropertyReference>
       }
 /** Extract property references from the given expression */
@@ -65,10 +70,17 @@ export function* extractPropertyReferences(
                 )
                 return
             }
-            yield { type: "unknown" }
+            yield { type: "unknown", node: ref.node }
         }
     }
 }
+/** Extract property references from the given pattern */
+export function* extractPropertyReferencesForPattern(
+    node: Pattern,
+    context: Rule.RuleContext,
+): Iterable<PropertyReference> {
+    yield* iteratePropertyReferencesForPattern(node, context)
+}
 
 /** Checks whether the given expression is shallow copied. */
 function isShallowCopy(
@@ -98,6 +110,7 @@ function* iteratePropertyReferencesForMemberExpression(
     if (property == null) {
         yield {
             type: "unknown",
+            node,
             *extractPropertyReferences() {
                 yield* extractPropertyReferences(node, context)
             },
@@ -107,6 +120,7 @@ function* iteratePropertyReferencesForMemberExpression(
     yield {
         type: "member",
         name: property,
+        node,
         *extractPropertyReferences() {
             yield* extractPropertyReferences(node, context)
         },
@@ -127,6 +141,7 @@ function* iteratePropertyReferencesForObjectPattern(
         if (property == null) {
             yield {
                 type: "unknown",
+                node,
                 *extractPropertyReferences() {
                     yield* iteratePropertyReferencesForPattern(
                         prop.value,
@@ -139,6 +154,7 @@ function* iteratePropertyReferencesForObjectPattern(
         yield {
             type: "destructuring",
             name: property,
+            node: prop,
             *extractPropertyReferences() {
                 yield* iteratePropertyReferencesForPattern(prop.value, context)
             },
@@ -170,6 +186,7 @@ function* iteratePropertyReferencesForArrayPattern(
         yield {
             type: "destructuring",
             name: String(index),
+            node: element,
             *extractPropertyReferences() {
                 yield* iteratePropertyReferencesForPattern(element, context)
             },
@@ -182,6 +199,7 @@ function* iteratePropertyReferencesForArrayPattern(
         }
         yield {
             type: "unknown",
+            node: element,
             *extractPropertyReferences() {
                 yield* iteratePropertyReferencesForPattern(element, context)
             },
@@ -196,6 +214,7 @@ function* iteratePropertyReferencesForForOf(
 ): Iterable<PropertyReference> {
     yield {
         type: "iteration",
+        node,
         *extractPropertyReferences() {
             let left = node.left
             if (left.type === "VariableDeclaration") {
@@ -227,7 +246,7 @@ function* iteratePropertyReferencesForPattern(
     } else if (target.type === "ArrayPattern") {
         yield* iteratePropertyReferencesForArrayPattern(target, context)
     } else {
-        yield { type: "unknown" }
+        yield { type: "unknown", node: target }
     }
 }
 
@@ -259,6 +278,7 @@ function* iteratePropertyReferencesForShallowCopy(
             )) {
                 yield {
                     type: "unknown",
+                    node: ref.node,
                     extractPropertyReferences: ref.extractPropertyReferences,
                 }
             }
diff --git a/lib/utils/extract-capturing-group-references.ts b/lib/utils/extract-capturing-group-references.ts
new file mode 100644
index 0000000..c5f6c54
--- /dev/null
+++ b/lib/utils/extract-capturing-group-references.ts
@@ -0,0 +1,632 @@
+import type { Rule } from "eslint"
+import type {
+    ArrowFunctionExpression,
+    CallExpression,
+    Expression,
+    FunctionExpression,
+    Literal,
+    MemberExpression,
+    Pattern,
+    RestElement,
+} from "estree"
+import type { ReadonlyFlags } from "regexp-ast-analysis"
+import type { KnownMethodCall, PropertyReference } from "./ast-utils"
+import {
+    getParent,
+    parseReplacements,
+    getStaticValue,
+    extractPropertyReferences,
+    extractExpressionReferences,
+    isKnownMethodCall,
+} from "./ast-utils"
+import { extractPropertyReferencesForPattern } from "./ast-utils/extract-property-references"
+import { parseReplacementsForString } from "./replacements-utils"
+import type { TypeTracker } from "./type-tracker"
+
+export type UnknownUsage = {
+    type: "UnknownUsage"
+    node: Expression
+    on?: "replace" | "replaceAll" | "matchAll"
+}
+export type WithoutRef = {
+    type: "WithoutRef"
+    node: Expression
+    on:
+        | "search"
+        | "test"
+        | "match"
+        | "replace"
+        | "replaceAll"
+        | "matchAll"
+        | "exec"
+}
+export type ArrayRef =
+    | {
+          type: "ArrayRef"
+          kind: "index"
+          ref: number | null /* null for unknown index access.  */
+          prop: PropertyReference & { type: "member" | "destructuring" }
+      }
+    | {
+          type: "ArrayRef"
+          kind: "name"
+          ref: string
+          prop: PropertyReference & { type: "member" | "destructuring" }
+      }
+    | {
+          type: "ArrayRef"
+          kind: "name"
+          ref: null /* null for unknown name access.  */
+          prop: PropertyReference & { type: "unknown" | "iteration" }
+      }
+export type ReplacementRef =
+    | {
+          type: "ReplacementRef"
+          kind: "index"
+          ref: number
+          range?: [number, number]
+      }
+    | {
+          type: "ReplacementRef"
+          kind: "name"
+          ref: string
+          range?: [number, number]
+      }
+export type ReplacerFunctionRef =
+    | {
+          type: "ReplacerFunctionRef"
+          kind: "index"
+          ref: number
+          arg: Pattern
+      }
+    | {
+          type: "ReplacerFunctionRef"
+          kind: "name"
+          ref: string
+          prop: PropertyReference & { type: "member" | "destructuring" }
+      }
+    | {
+          type: "ReplacerFunctionRef"
+          kind: "name"
+          ref: null /* null for unknown name access.  */
+          prop: PropertyReference & { type: "unknown" | "iteration" }
+          arg: null
+      }
+    | {
+          type: "ReplacerFunctionRef"
+          kind: "name"
+          ref: null /* null for unknown name access.  */
+          prop: null
+          arg: Pattern
+      }
+    | {
+          type: "ReplacerFunctionRef"
+          kind: "unknown"
+          ref: null /* null for unknown access.  */
+          arg: Pattern
+      }
+export type Split = {
+    type: "Split"
+    node: CallExpression
+}
+export type UnknownRef =
+    | {
+          type: "UnknownRef"
+          kind: "array"
+          prop: PropertyReference & { type: "unknown" | "iteration" }
+      }
+    | {
+          type: "UnknownRef"
+          kind: "replacerFunction"
+          arg: RestElement
+      }
+export type CapturingGroupReference =
+    | ArrayRef
+    | ReplacementRef
+    | ReplacerFunctionRef
+    | UnknownRef
+    | WithoutRef
+    | Split
+    | UnknownUsage
+
+type ExtractCapturingGroupReferencesContext = {
+    flags: ReadonlyFlags
+    typeTracer: TypeTracker
+    countOfCapturingGroup: number
+    context: Rule.RuleContext
+}
+
+/**
+ * Extracts the usage of the capturing group.
+ */
+export function* extractCapturingGroupReferences(
+    node: Expression,
+    flags: ReadonlyFlags,
+    typeTracer: TypeTracker,
+    countOfCapturingGroup: number,
+    context: Rule.RuleContext,
+): Iterable<CapturingGroupReference> {
+    const ctx = {
+        flags,
+        typeTracer,
+        countOfCapturingGroup,
+        context,
+    }
+    for (const ref of extractExpressionReferences(node, context)) {
+        if (ref.type === "argument") {
+            yield* iterateForArgument(ref.callExpression, ref.node, ctx)
+        } else if (ref.type === "member") {
+            yield* iterateForMember(ref.memberExpression, ref.node, ctx)
+        } else {
+            yield {
+                type: "UnknownUsage",
+                node: ref.node,
+            }
+        }
+    }
+}
+
+/** Iterate the capturing group references for given argument expression node. */
+function* iterateForArgument(
+    callExpression: CallExpression,
+    argument: Expression,
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    if (
+        !isKnownMethodCall(callExpression, {
+            match: 1,
+            search: 1,
+            replace: 2,
+            replaceAll: 2,
+            matchAll: 1,
+            split: 1,
+        })
+    ) {
+        return
+    }
+    if (callExpression.arguments[0] !== argument) {
+        return
+    }
+    if (!ctx.typeTracer.isString(callExpression.callee.object)) {
+        yield {
+            type: "UnknownUsage",
+            node: argument,
+        }
+        return
+    }
+
+    if (callExpression.callee.property.name === "match") {
+        yield* iterateForStringMatch(callExpression, argument, ctx)
+    } else if (callExpression.callee.property.name === "search") {
+        yield {
+            type: "WithoutRef",
+            node: argument,
+            on: "search",
+        }
+    } else if (
+        callExpression.callee.property.name === "replace" ||
+        callExpression.callee.property.name === "replaceAll"
+    ) {
+        yield* iterateForStringReplace(
+            callExpression,
+            argument,
+            ctx,
+            callExpression.callee.property.name,
+        )
+    } else if (callExpression.callee.property.name === "matchAll") {
+        yield* iterateForStringMatchAll(callExpression, argument, ctx)
+    } else if (callExpression.callee.property.name === "split") {
+        yield {
+            type: "Split",
+            node: callExpression,
+        }
+    }
+}
+
+/** Iterate the capturing group references for given member expression node. */
+function* iterateForMember(
+    memberExpression: MemberExpression,
+    object: Expression,
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    const parent = getParent(memberExpression)
+    if (
+        !parent ||
+        parent.type !== "CallExpression" ||
+        parent.callee !== memberExpression ||
+        !isKnownMethodCall(parent, {
+            test: 1,
+            exec: 1,
+        })
+    ) {
+        return
+    }
+    if (parent.callee.property.name === "test") {
+        yield {
+            type: "WithoutRef",
+            node: object,
+            on: "test",
+        }
+    } else if (parent.callee.property.name === "exec") {
+        yield* iterateForRegExpExec(parent, object, ctx)
+    }
+}
+
+/** Iterate the capturing group references for String.prototype.match(). */
+function* iterateForStringMatch(
+    node: KnownMethodCall,
+    argument: Expression,
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    if (ctx.flags.global) {
+        // String.prototype.match() with g flag
+        yield {
+            type: "WithoutRef",
+            node: argument,
+            on: "match",
+        }
+    } else {
+        // String.prototype.match() without g flag
+        let useRet = false
+        for (const ref of iterateForExecResult(node, ctx)) {
+            useRet = true
+            yield ref
+        }
+        if (!useRet) {
+            yield {
+                type: "WithoutRef",
+                node: argument,
+                on: "match",
+            }
+        }
+    }
+}
+
+/** Iterate the capturing group references for String.prototype.replace() and String.prototype.replaceAll(). */
+function* iterateForStringReplace(
+    node: KnownMethodCall,
+    argument: Expression,
+    ctx: ExtractCapturingGroupReferencesContext,
+    on: "replace" | "replaceAll",
+): Iterable<CapturingGroupReference> {
+    const replacementNode = node.arguments[1]
+    if (
+        replacementNode.type === "FunctionExpression" ||
+        replacementNode.type === "ArrowFunctionExpression"
+    ) {
+        // replacer function
+        yield* iterateForReplacerFunction(replacementNode, argument, on, ctx)
+    } else {
+        const replacement = node.arguments[1]
+        if (!replacement) {
+            yield {
+                type: "UnknownUsage",
+                node: argument,
+                on,
+            }
+            return
+        }
+        if (replacement.type === "Literal") {
+            yield* verifyForReplaceReplacementLiteral(
+                replacement,
+                argument,
+                on,
+                ctx,
+            )
+        } else {
+            const evaluated = getStaticValue(ctx.context, replacement)
+            if (!evaluated || typeof evaluated.value !== "string") {
+                yield {
+                    type: "UnknownUsage",
+                    node: argument,
+                    on,
+                }
+                return
+            }
+            yield* verifyForReplaceReplacement(evaluated.value, argument, on)
+        }
+    }
+}
+
+/** Iterate the capturing group references for String.prototype.matchAll(). */
+function* iterateForStringMatchAll(
+    node: KnownMethodCall,
+    argument: Expression,
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    let useRet = false
+    for (const iterationRef of extractPropertyReferences(node, ctx.context)) {
+        if (!iterationRef.extractPropertyReferences) {
+            useRet = true
+            yield {
+                type: "UnknownUsage",
+                node: argument,
+                on: "matchAll",
+            }
+            return
+        }
+        if (hasNameRef(iterationRef)) {
+            if (Number.isNaN(Number(iterationRef.name))) {
+                // Not aimed to iteration.
+                continue
+            }
+        }
+        for (const ref of iterationRef.extractPropertyReferences()) {
+            if (hasNameRef(ref)) {
+                if (ref.name === "groups") {
+                    for (const namedRef of ref.extractPropertyReferences()) {
+                        useRet = true
+                        yield getNamedArrayRef(namedRef)
+                    }
+                } else {
+                    if (
+                        ref.name === "input" ||
+                        ref.name === "index" ||
+                        ref.name === "indices"
+                    ) {
+                        continue
+                    }
+                    useRet = true
+                    yield getIndexArrayRef(ref)
+                }
+            } else {
+                useRet = true
+                yield {
+                    type: "UnknownRef",
+                    kind: "array",
+                    prop: ref,
+                }
+                return
+            }
+        }
+    }
+    if (!useRet) {
+        yield {
+            type: "WithoutRef",
+            node: argument,
+            on: "matchAll",
+        }
+    }
+}
+
+/** Iterate the capturing group references for RegExp.prototype.exec() . */
+function* iterateForRegExpExec(
+    node: KnownMethodCall,
+    object: Expression,
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    let useRet = false
+    for (const ref of iterateForExecResult(node, ctx)) {
+        useRet = true
+        yield ref
+    }
+    if (!useRet) {
+        yield {
+            type: "WithoutRef",
+            node: object,
+            on: "exec",
+        }
+    }
+}
+
+/** Iterate the capturing group references for RegExp.prototype.exec() and String.prototype.match() result */
+function* iterateForExecResult(
+    node: KnownMethodCall,
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    for (const ref of extractPropertyReferences(node, ctx.context)) {
+        if (hasNameRef(ref)) {
+            if (ref.name === "groups") {
+                for (const namedRef of ref.extractPropertyReferences()) {
+                    yield getNamedArrayRef(namedRef)
+                }
+            } else {
+                if (
+                    ref.name === "input" ||
+                    ref.name === "index" ||
+                    ref.name === "indices"
+                ) {
+                    continue
+                }
+                yield getIndexArrayRef(ref)
+            }
+        } else {
+            yield {
+                type: "UnknownRef",
+                kind: "array",
+                prop: ref,
+            }
+            return
+        }
+    }
+}
+
+/** Iterate the capturing group references for String.prototype.replace(regexp, "str") and String.prototype.replaceAll(regexp, "str") */
+function* verifyForReplaceReplacementLiteral(
+    substr: Literal,
+    argument: Expression,
+    on: "replace" | "replaceAll",
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    let useReplacement = false
+    for (const replacement of parseReplacements(ctx.context, substr)) {
+        if (replacement.type === "ReferenceElement") {
+            useReplacement = true
+            if (typeof replacement.ref === "number") {
+                yield {
+                    type: "ReplacementRef",
+                    kind: "index",
+                    ref: replacement.ref,
+                    range: replacement.range,
+                }
+            } else {
+                yield {
+                    type: "ReplacementRef",
+                    kind: "name",
+                    ref: replacement.ref,
+                    range: replacement.range,
+                }
+            }
+        }
+    }
+    if (!useReplacement) {
+        yield {
+            type: "WithoutRef",
+            node: argument,
+            on,
+        }
+    }
+}
+
+/** Iterate the capturing group references for String.prototype.replace(regexp, str) and String.prototype.replaceAll(regexp, str) */
+function* verifyForReplaceReplacement(
+    substr: string,
+    argument: Expression,
+    on: "replace" | "replaceAll",
+): Iterable<CapturingGroupReference> {
+    let useReplacement = false
+    for (const replacement of parseReplacementsForString(substr)) {
+        if (replacement.type === "ReferenceElement") {
+            useReplacement = true
+            if (typeof replacement.ref === "number") {
+                yield {
+                    type: "ReplacementRef",
+                    kind: "index",
+                    ref: replacement.ref,
+                }
+            } else {
+                yield {
+                    type: "ReplacementRef",
+                    kind: "name",
+                    ref: replacement.ref,
+                }
+            }
+        }
+    }
+    if (!useReplacement) {
+        yield {
+            type: "WithoutRef",
+            node: argument,
+            on,
+        }
+    }
+}
+
+/** Iterate the capturing group references for String.prototype.replace(regexp, fn) and String.prototype.replaceAll(regexp, fn) */
+function* iterateForReplacerFunction(
+    replacementNode: FunctionExpression | ArrowFunctionExpression,
+    argument: Expression,
+    on: "replace" | "replaceAll",
+    ctx: ExtractCapturingGroupReferencesContext,
+): Iterable<CapturingGroupReference> {
+    if (replacementNode.params.length < 2) {
+        yield {
+            type: "WithoutRef",
+            node: argument,
+            on,
+        }
+        return
+    }
+    for (let index = 0; index < replacementNode.params.length; index++) {
+        const arg = replacementNode.params[index]
+        if (arg.type === "RestElement") {
+            yield {
+                type: "UnknownRef",
+                kind: "replacerFunction",
+                arg,
+            }
+            return
+        }
+        if (index === 0) {
+            continue
+        } else if (index <= ctx.countOfCapturingGroup) {
+            yield {
+                type: "ReplacerFunctionRef",
+                kind: "index",
+                ref: index,
+                arg,
+            }
+        } else if (ctx.countOfCapturingGroup + 3 === index) {
+            if (arg.type === "Identifier" || arg.type === "ObjectPattern") {
+                for (const ref of extractPropertyReferencesForPattern(
+                    arg,
+                    ctx.context,
+                )) {
+                    if (hasNameRef(ref)) {
+                        yield {
+                            type: "ReplacerFunctionRef",
+                            kind: "name",
+                            ref: ref.name,
+                            prop: ref,
+                        }
+                    } else {
+                        yield {
+                            type: "ReplacerFunctionRef",
+                            kind: "name",
+                            ref: null,
+                            prop: ref,
+                            arg: null,
+                        }
+                    }
+                }
+            } else {
+                yield {
+                    type: "ReplacerFunctionRef",
+                    kind: "name",
+                    ref: null,
+                    arg,
+                    prop: null,
+                }
+            }
+        }
+    }
+}
+
+/** Checks whether the given reference is a named reference. */
+function hasNameRef(
+    ref: PropertyReference,
+): ref is PropertyReference & { type: "member" | "destructuring" } {
+    return ref.type === "destructuring" || ref.type === "member"
+}
+
+/** Get the index array ref from PropertyReference */
+function getIndexArrayRef(
+    ref: PropertyReference & { type: "member" | "destructuring" },
+): CapturingGroupReference {
+    const numRef = Number(ref.name)
+    if (Number.isFinite(numRef)) {
+        return {
+            type: "ArrayRef",
+            kind: "index",
+            ref: numRef,
+            prop: ref,
+        }
+    }
+    return {
+        type: "ArrayRef",
+        kind: "index",
+        ref: null,
+        prop: ref,
+    }
+}
+
+/** Get the named array ref from PropertyReference */
+function getNamedArrayRef(
+    namedRef: PropertyReference,
+): CapturingGroupReference {
+    if (hasNameRef(namedRef)) {
+        return {
+            type: "ArrayRef",
+            kind: "name",
+            ref: namedRef.name,
+            prop: namedRef,
+        }
+    }
+    // unknown used as name
+    return {
+        type: "ArrayRef",
+        kind: "name",
+        ref: null,
+        prop: namedRef,
+    }
+}
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 6177b34..a529f85 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -1,7 +1,13 @@
 import type * as ESTree from "estree"
 import type { RuleListener, RuleModule, PartialRuleModule } from "../types"
 import type { RegExpVisitor } from "regexpp/visitor"
-import type { Element, Node, Pattern, Quantifier } from "regexpp/ast"
+import type {
+    CapturingGroup,
+    Element,
+    Node,
+    Pattern,
+    Quantifier,
+} from "regexpp/ast"
 import { RegExpParser, visitRegExpAST } from "regexpp"
 import { CALL, CONSTRUCT, ReferenceTracker } from "eslint-utils"
 import type { Rule, AST, SourceCode } from "eslint"
@@ -20,6 +26,9 @@ import {
 } from "./ast-utils/utils"
 import type { PatternRange } from "./ast-utils/pattern-source"
 import { PatternSource } from "./ast-utils/pattern-source"
+import type { CapturingGroupReference } from "./extract-capturing-group-references"
+import { extractCapturingGroupReferences } from "./extract-capturing-group-references"
+import { createTypeTracker } from "./type-tracker"
 export * from "./unicode"
 
 type RegExpContextBase = {
@@ -79,6 +88,16 @@ type RegExpContextBase = {
      */
     getUsageOfPattern: () => UsageOfPattern
 
+    /**
+     * Returns the capturing group references
+     */
+    getCapturingGroupReferences: () => CapturingGroupReference[]
+
+    /**
+     * Returns a list of all capturing groups in the order of their numbers.
+     */
+    getAllCapturingGroups: () => CapturingGroup[]
+
     pattern: string
     patternAst: Pattern
     patternSource: PatternSource
@@ -591,6 +610,8 @@ function buildRegExpContextBase({
     const sourceCode = context.getSourceCode()
 
     let cacheUsageOfPattern: UsageOfPattern | null = null
+    let cacheCapturingGroupReference: CapturingGroupReference[] | null = null
+    let cacheAllCapturingGroups: CapturingGroup[] | null = null
     return {
         getRegexpLocation: (range, offsets) => {
             if (offsets) {
@@ -622,12 +643,36 @@ function buildRegExpContextBase({
         },
         getUsageOfPattern: () =>
             (cacheUsageOfPattern ??= getUsageOfPattern(regexpNode, context)),
+        getCapturingGroupReferences: () => {
+            if (cacheCapturingGroupReference) {
+                return cacheCapturingGroupReference
+            }
+            const countOfCapturingGroup = getAllCapturingGroupsWithCache()
+                .length
+            return (cacheCapturingGroupReference = [
+                ...extractCapturingGroupReferences(
+                    regexpNode,
+                    flags,
+                    createTypeTracker(context),
+                    countOfCapturingGroup,
+                    context,
+                ),
+            ])
+        },
+        getAllCapturingGroups: getAllCapturingGroupsWithCache,
 
         pattern: parsedPattern.raw,
         patternAst: parsedPattern,
         patternSource,
         flags: toCache(flags),
     }
+
+    /** Returns a list of all capturing groups in the order of their numbers. */
+    function getAllCapturingGroupsWithCache(): CapturingGroup[] {
+        return (cacheAllCapturingGroups ??= getAllCapturingGroups(
+            parsedPattern,
+        ))
+    }
 }
 
 /**
@@ -1217,3 +1262,20 @@ export function isUseHexEscape(raw: string): boolean {
 export function isEscapeSequence(raw: string): boolean {
     return Boolean(getEscapeSequenceKind(raw))
 }
+
+/** 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
+}
diff --git a/lib/utils/type-tracker/index.ts b/lib/utils/type-tracker/index.ts
index 257294d..19f2dae 100644
--- a/lib/utils/type-tracker/index.ts
+++ b/lib/utils/type-tracker/index.ts
@@ -44,7 +44,7 @@ const ts: typeof import("typescript") = (() => {
     }
 })()
 
-type TypeTracker = {
+export type TypeTracker = {
     isString: (node: ES.Expression) => boolean
     maybeString: (node: ES.Expression) => boolean
     isRegExp: (node: ES.Expression) => boolean


Test patch:
diff --git a/tests/lib/rules/no-unused-capturing-group.ts b/tests/lib/rules/no-unused-capturing-group.ts
index ebb9612..dd113ae 100644
--- a/tests/lib/rules/no-unused-capturing-group.ts
+++ b/tests/lib/rules/no-unused-capturing-group.ts
@@ -39,13 +39,11 @@ tester.run("no-unused-capturing-group", rule as any, {
         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}`)",
-        "var replaced = '2000-12-31'.replace(/(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})/, (_, y, m, d, o, s, g) => `${g.y}/${g.m}/${g.d}`)",
         String.raw`
         // var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2')
         var replaced = '2000-12-31'.replace(/\d{4}-\d{2}-\d{2}/, '$1/$2')
         var replaced = (20001231).replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2')
         var replaced = '2000-12-31'.replace(unknown, '$1/$2')
-        var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, 1234)
         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')
@@ -114,6 +112,13 @@ tester.run("no-unused-capturing-group", rule as any, {
                 sourceType: "script",
             },
         },
+        // Unused names.
+        "var replaced = '2000-12-31'.replace(/(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})/, (_, y, m, d) => `${y}/${m}/${d}`)",
+        "var replaced = '2000-12-31'.replace(/(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})/, function (_, y, m, d) {return `${y}/${m}/${d}`})",
+        "var replaced = '2000-12-31'.replaceAll(/(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})/g, (_, y, m, d) => `${y}/${m}/${d}`)",
+        "var replaced = '2000-12-31'.replace(/(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})/, (_, y, m, d, o, s, g) => `${g.y}/${g.m}/${g.d}`)",
+        String.raw`'2000-12-31'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/u, '$1/$2/$3') // "2000/12/31"`,
+        String.raw`const [,y,m,d] = '2000-12-31'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)`,
     ],
     invalid: [
         {
@@ -138,16 +143,6 @@ tester.run("no-unused-capturing-group", rule as any, {
             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: [
-                "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: [
@@ -177,15 +172,6 @@ tester.run("no-unused-capturing-group", rule as any, {
                 "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: [
-                "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`
             var replaced = '2000-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$4/$5/$6')
@@ -258,16 +244,6 @@ tester.run("no-unused-capturing-group", rule as any, {
             `,
             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: [
-                "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"]
@@ -309,10 +285,7 @@ tester.run("no-unused-capturing-group", rule as any, {
                 // var d = matches[3] // "31"
             }
             `,
-            errors: [
-                "Capturing group 'y' has a name, but its name is never used.",
-                "Capturing group 'd' is defined but never used.",
-            ],
+            errors: ["Capturing group 'd' is defined but never used."],
         },
         {
             code: String.raw`
@@ -323,12 +296,8 @@ tester.run("no-unused-capturing-group", rule as any, {
                 var m = matches.groups.m // "12"
                 // var d = matches[3] // "31"
             }
-
             `,
-            errors: [
-                "Capturing group 'y' has a name, but its name is never used.",
-                "Capturing group 'd' is defined but never used.",
-            ],
+            errors: ["Capturing group 'd' is defined but never used."],
         },
         {
             code: String.raw`
@@ -341,10 +310,7 @@ tester.run("no-unused-capturing-group", rule as any, {
                 // var d = matches[3] // "31"
             }
             `,
-            errors: [
-                "Capturing group 'y' has a name, but its name is never used.",
-                "Capturing group number 3 is defined but never used.",
-            ],
+            errors: ["Capturing group number 3 is defined but never used."],
         },
         {
             code: String.raw`
@@ -355,10 +321,7 @@ tester.run("no-unused-capturing-group", rule as any, {
                 // var d = matches[3] // "31"
             }
             `,
-            errors: [
-                "Capturing group 'y' has a name, but its name is never used.",
-                "Capturing group number 3 is defined but never used.",
-            ],
+            errors: ["Capturing group number 3 is defined but never used."],
         },
         {
             code: String.raw`
@@ -374,9 +337,7 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                "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.",
             ],
         },
@@ -391,24 +352,6 @@ tester.run("no-unused-capturing-group", rule as any, {
             }
             `,
             errors: [
-                {
-                    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.",
@@ -429,27 +372,6 @@ tester.run("no-unused-capturing-group", rule as any, {
                 },
             ],
         },
-        {
-            code: String.raw`
-            const reg = /(?<comma>,)/;
-            'a,b,c'.split(reg)
-            `,
-            errors: [
-                {
-                    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: [