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

Issue:
Add `no-useless-string-literal`
**Description**
<!--
A clear and concise description of the new feature.
Also give a few code examples.
-->

`no-useless-string-literal` rule reports the string alternatives of a single character.
They can be placed outside `\q{...}`.

```js
/* ✗ BAD */
var foo = /[\q{a}]/v // => /[a]/v
var foo = /[\q{a|bc}]/v // => /[a\q{bc}]/v

/* ✓ GOOD */
var foo = /[\q{ab|}]/v
```




Pull request description:
Add `regexp/no-useless-string-literal` rule
close #638

Patch:
diff --git a/.changeset/green-bananas-tie.md b/.changeset/green-bananas-tie.md
new file mode 100644
index 0000000..05b87b9
--- /dev/null
+++ b/.changeset/green-bananas-tie.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": major
+---
+
+Add `regexp/no-useless-string-literal` rule
diff --git a/.changeset/purple-suns-carry.md b/.changeset/purple-suns-carry.md
new file mode 100644
index 0000000..12488cb
--- /dev/null
+++ b/.changeset/purple-suns-carry.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/no-empty-character-class`
diff --git a/README.md b/README.md
index 2f04610..63f64d0 100644
--- a/README.md
+++ b/README.md
@@ -157,6 +157,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
 | [no-useless-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html)                                 | disallow quantifiers that can be removed                                                   | ✅  |    | 🔧 | 💡 |
 | [no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html)                                           | disallow unnecessary character ranges                                                      | ✅  |    | 🔧 |    |
 | [no-useless-set-operand](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-set-operand.html)                               | disallow unnecessary elements in expression character classes                              | ✅  |    | 🔧 |    |
+| [no-useless-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-string-literal.html)                         | disallow string disjunction of single characters in `\q{...}`                              | ✅  |    | 🔧 |    |
 | [no-useless-two-nums-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html)               | disallow unnecessary `{n,m}` quantifier                                                    | ✅  |    | 🔧 |    |
 | [no-zero-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-zero-quantifier.html)                                       | disallow quantifiers with a maximum of zero                                                | ✅  |    |    | 💡 |
 | [optimal-lookaround-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/optimal-lookaround-quantifier.html)                 | disallow the alternatives of lookarounds that end with a non-constant quantifier           |    | ✅  |    | 💡 |
diff --git a/docs/rules/index.md b/docs/rules/index.md
index f293137..9e3954a 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -64,6 +64,7 @@ sidebarDepth: 0
 | [no-useless-quantifier](no-useless-quantifier.md)                                 | disallow quantifiers that can be removed                                                   | ✅  |    | 🔧 | 💡 |
 | [no-useless-range](no-useless-range.md)                                           | disallow unnecessary character ranges                                                      | ✅  |    | 🔧 |    |
 | [no-useless-set-operand](no-useless-set-operand.md)                               | disallow unnecessary elements in expression character classes                              | ✅  |    | 🔧 |    |
+| [no-useless-string-literal](no-useless-string-literal.md)                         | disallow string disjunction of single characters in `\q{...}`                              | ✅  |    | 🔧 |    |
 | [no-useless-two-nums-quantifier](no-useless-two-nums-quantifier.md)               | disallow unnecessary `{n,m}` quantifier                                                    | ✅  |    | 🔧 |    |
 | [no-zero-quantifier](no-zero-quantifier.md)                                       | disallow quantifiers with a maximum of zero                                                | ✅  |    |    | 💡 |
 | [optimal-lookaround-quantifier](optimal-lookaround-quantifier.md)                 | disallow the alternatives of lookarounds that end with a non-constant quantifier           |    | ✅  |    | 💡 |
diff --git a/docs/rules/no-useless-string-literal.md b/docs/rules/no-useless-string-literal.md
new file mode 100644
index 0000000..f9a3113
--- /dev/null
+++ b/docs/rules/no-useless-string-literal.md
@@ -0,0 +1,57 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-useless-string-literal"
+description: "disallow string disjunction of single characters in `\\q{...}`"
+---
+# regexp/no-useless-string-literal
+
+💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.
+
+🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
+<!-- end auto-generated rule header -->
+
+> disallow string disjunction of single characters in `\q{...}`
+
+## :book: Rule Details
+
+This rule reports the string alternatives of a single character in `\q{...}`.
+It can be placed outside `\q{...}`.
+
+<eslint-code-block fix>
+
+```js
+/* eslint regexp/no-useless-string-literal: "error" */
+
+/* ✓ GOOD */
+var foo = /[\q{abc}]/v
+var foo = /[\q{ab|}]/v
+
+/* ✗ BAD */
+var foo = /[\q{a}]/v // => /[a]/v
+var foo = /[\q{a|bc}]/v // => /[a\q{bc}]/v
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related rules
+
+- [regexp/no-empty-alternative]
+- [regexp/no-empty-string-literal]
+
+[regexp/no-empty-alternative]: ./no-empty-alternative.md
+[regexp/no-empty-string-literal]: ./no-empty-string-literal.md
+
+## :rocket: Version
+
+:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-string-literal.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-string-literal.ts)
diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts
index 95a9d0b..eb1b004 100644
--- a/lib/configs/recommended.ts
+++ b/lib/configs/recommended.ts
@@ -52,6 +52,7 @@ export const rules = {
     "regexp/no-useless-quantifier": "error",
     "regexp/no-useless-range": "error",
     "regexp/no-useless-set-operand": "error",
+    "regexp/no-useless-string-literal": "error",
     "regexp/no-useless-two-nums-quantifier": "error",
     "regexp/no-zero-quantifier": "error",
     "regexp/optimal-lookaround-quantifier": "warn",
diff --git a/lib/rules/no-empty-character-class.ts b/lib/rules/no-empty-character-class.ts
index 4ecb959..2ce8637 100644
--- a/lib/rules/no-empty-character-class.ts
+++ b/lib/rules/no-empty-character-class.ts
@@ -35,6 +35,15 @@ export default createRule("no-empty-character-class", {
                         })
                     }
                 },
+                onExpressionCharacterClassEnter(ccNode) {
+                    if (matchesNoCharacters(ccNode, flags)) {
+                        context.report({
+                            node,
+                            loc: getRegexpLocation(ccNode),
+                            messageId: "cannotMatchAny",
+                        })
+                    }
+                },
             }
         }
 
diff --git a/lib/rules/no-useless-string-literal.ts b/lib/rules/no-useless-string-literal.ts
new file mode 100644
index 0000000..f4243a4
--- /dev/null
+++ b/lib/rules/no-useless-string-literal.ts
@@ -0,0 +1,106 @@
+import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import type {
+    ClassStringDisjunction,
+    StringAlternative,
+} from "@eslint-community/regexpp/ast"
+import { RESERVED_DOUBLE_PUNCTUATOR_CHARS } from "../utils/unicode-set"
+
+export default createRule("no-useless-string-literal", {
+    meta: {
+        docs: {
+            description:
+                "disallow string disjunction of single characters in `\\q{...}`",
+            category: "Best Practices",
+            recommended: true,
+        },
+        schema: [],
+        messages: {
+            unexpected: "Unexpected string disjunction of single character.",
+        },
+        type: "suggestion",
+        fixable: "code",
+    },
+    create(context) {
+        function createVisitor(
+            regexpContext: RegExpContext,
+        ): RegExpVisitor.Handlers {
+            const { node, getRegexpLocation, fixReplaceNode, pattern } =
+                regexpContext
+            return {
+                onStringAlternativeEnter(saNode) {
+                    if (saNode.elements.length === 1) {
+                        const csdNode = saNode.parent
+                        context.report({
+                            node,
+                            loc: getRegexpLocation(saNode),
+                            messageId: "unexpected",
+                            fix: fixReplaceNode(csdNode, () => {
+                                const alternativesText = csdNode.alternatives
+                                    .filter((alt) => alt !== saNode)
+                                    .map((alt) => alt.raw)
+                                    .join("|")
+                                if (!alternativesText.length) {
+                                    const escape =
+                                        isNeedEscapedInCharacterClass(
+                                            csdNode,
+                                            saNode,
+                                        )
+                                            ? "\\"
+                                            : ""
+                                    return `${escape}${saNode.raw}`
+                                }
+                                if (
+                                    csdNode.parent.type ===
+                                        "ClassIntersection" ||
+                                    csdNode.parent.type === "ClassSubtraction"
+                                ) {
+                                    const escape =
+                                        saNode.raw === "^" ? "\\" : ""
+                                    return String.raw`[${escape}${saNode.raw}\q{${alternativesText}}]`
+                                }
+                                const escape = isNeedEscapedInCharacterClass(
+                                    csdNode,
+                                    saNode,
+                                )
+                                    ? "\\"
+                                    : ""
+                                return String.raw`${escape}${saNode.raw}\q{${alternativesText}}`
+                            }),
+                        })
+                    }
+                },
+            }
+
+            /**
+             * Checks whether an escape is required if the given character when placed before a \q{...}
+             */
+            function isNeedEscapedInCharacterClass(
+                disjunction: ClassStringDisjunction,
+                character: StringAlternative,
+            ) {
+                const char = character.raw
+                // Avoid [A&&\q{&}] => [A&&&]
+                if (
+                    RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
+                    // The previous character is the same
+                    pattern[disjunction.start - 1] === char
+                ) {
+                    return true
+                }
+
+                // Avoid [\q{^|ab}] => [^\q{ab}]
+                return (
+                    char === "^" &&
+                    disjunction.parent.type === "CharacterClass" &&
+                    disjunction.parent.start === disjunction.start - 1
+                )
+            }
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
+    },
+})
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index ca08d86..220ba93 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -49,6 +49,7 @@ import noUselessNonGreedy from "../rules/no-useless-non-greedy"
 import noUselessQuantifier from "../rules/no-useless-quantifier"
 import noUselessRange from "../rules/no-useless-range"
 import noUselessSetOperand from "../rules/no-useless-set-operand"
+import noUselessStringLiteral from "../rules/no-useless-string-literal"
 import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
 import noZeroQuantifier from "../rules/no-zero-quantifier"
 import optimalLookaroundQuantifier from "../rules/optimal-lookaround-quantifier"
@@ -135,6 +136,7 @@ export const rules = [
     noUselessQuantifier,
     noUselessRange,
     noUselessSetOperand,
+    noUselessStringLiteral,
     noUselessTwoNumsQuantifier,
     noZeroQuantifier,
     optimalLookaroundQuantifier,


Test patch:
diff --git a/tests/lib/rules/no-empty-capturing-group.ts b/tests/lib/rules/no-empty-capturing-group.ts
index b66b6d8..ab8c395 100644
--- a/tests/lib/rules/no-empty-capturing-group.ts
+++ b/tests/lib/rules/no-empty-capturing-group.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/no-empty-capturing-group"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-empty-capturing-group", rule as any, {
-    valid: ["/(a)/", "/a(\\bb)/", "/a(\\b|b)/"],
+    valid: ["/(a)/", "/a(\\bb)/", "/a(\\b|b)/", String.raw`/a([\q{a}])/v`],
     invalid: [
         {
             code: "/a(\\b)/",
@@ -55,5 +55,14 @@ tester.run("no-empty-capturing-group", rule as any, {
             code: "/(\\b\\b|(?:\\B|$))a/",
             errors: ["Unexpected capture empty."],
         },
+        {
+            code: String.raw`/a([\q{}])/v`,
+            errors: [
+                {
+                    message: "Unexpected capture empty.",
+                    line: 1,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-empty-character-class.ts b/tests/lib/rules/no-empty-character-class.ts
index 8b5e229..5dfa401 100644
--- a/tests/lib/rules/no-empty-character-class.ts
+++ b/tests/lib/rules/no-empty-character-class.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-empty-character-class"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -21,6 +21,8 @@ tester.run("no-empty-character-class", rule as any, {
         `/[ ]/`,
         String.raw`/[\s\S]/`,
         String.raw`/[\da-zA-Z_\W]/`,
+        String.raw`/a[[[ab]&&b]]/v`,
+        String.raw`/a[[ab]&&b]/v`,
     ],
     invalid: [
         {
@@ -75,5 +77,33 @@ tester.run("no-empty-character-class", rule as any, {
             code: String.raw`/[^\da-zA-Z_\W]/`,
             errors: ["This character class cannot match any characters."],
         },
+        {
+            code: String.raw`/a[[a&&b]]/v`,
+            errors: [
+                {
+                    message:
+                        "This character class cannot match any characters.",
+                    line: 1,
+                    column: 3,
+                },
+                {
+                    message:
+                        "This character class cannot match any characters.",
+                    line: 1,
+                    column: 4,
+                },
+            ],
+        },
+        {
+            code: String.raw`/a[a&&b]/v`,
+            errors: [
+                {
+                    message:
+                        "This character class cannot match any characters.",
+                    line: 1,
+                    column: 3,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-invalid-regexp.ts b/tests/lib/rules/no-invalid-regexp.ts
index 27626f4..0b887d0 100644
--- a/tests/lib/rules/no-invalid-regexp.ts
+++ b/tests/lib/rules/no-invalid-regexp.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-invalid-regexp"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -63,5 +63,15 @@ tester.run("no-invalid-regexp", rule as any, {
                 },
             ],
         },
+        {
+            code: "new RegExp('[A&&&]', 'v');",
+            errors: [
+                {
+                    message:
+                        "Invalid regular expression: /[A&&&]/v: Invalid character in character class",
+                    column: 16,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-useless-dollar-replacements.ts b/tests/lib/rules/no-useless-dollar-replacements.ts
index ebff3fa..b63a843 100644
--- a/tests/lib/rules/no-useless-dollar-replacements.ts
+++ b/tests/lib/rules/no-useless-dollar-replacements.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-useless-dollar-replacements"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -91,6 +91,10 @@ tester.run("no-useless-dollar-replacements", rule as any, {
         regexp = 'a'
         "abc".replaceAll(regexp, 'foo');
         `,
+        String.raw`
+        const str = 'John Smith';
+        var newStr = str.replace(/(\w+)\s(\w+)/v, '$2, $1');
+        `,
     ],
     invalid: [
         {
@@ -171,5 +175,19 @@ tester.run("no-useless-dollar-replacements", rule as any, {
                 "'$<a>' replacement will insert '$<a>' because named capturing group does not found. Use '$$' if you want to escape '$'.",
             ],
         },
+        {
+            code: String.raw`
+            const str = 'John Smith';
+            var newStr = str.replace(/([[\w]]+)[\s](\w+)/v, '$3, $1 $2');
+            `,
+            errors: [
+                {
+                    message:
+                        "'$3' replacement will insert '$3' because there are less than 3 capturing groups. Use '$$' if you want to escape '$'.",
+                    line: 3,
+                    column: 62,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-useless-string-literal.ts b/tests/lib/rules/no-useless-string-literal.ts
new file mode 100644
index 0000000..764f270
--- /dev/null
+++ b/tests/lib/rules/no-useless-string-literal.ts
@@ -0,0 +1,88 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-useless-string-literal"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: "latest",
+        sourceType: "module",
+    },
+})
+
+tester.run("no-useless-string-literal", rule as any, {
+    valid: [String.raw`/[\q{abc}]/v`, String.raw`/[\q{ab|}]/v`],
+    invalid: [
+        {
+            code: String.raw`/[\q{a}]/v`,
+            output: String.raw`/[a]/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected string disjunction of single character.",
+                    line: 1,
+                    column: 6,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{a|bc}]/v`,
+            output: String.raw`/[a\q{bc}]/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected string disjunction of single character.",
+                    line: 1,
+                    column: 6,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{ab|c}]/v`,
+            output: String.raw`/[c\q{ab}]/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected string disjunction of single character.",
+                    line: 1,
+                    column: 9,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{ab|c|de}]/v`,
+            output: String.raw`/[c\q{ab|de}]/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected string disjunction of single character.",
+                    line: 1,
+                    column: 9,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[a\q{ab|\-}]/v`,
+            output: String.raw`/[a\-\q{ab}]/v`,
+            errors: ["Unexpected string disjunction of single character."],
+        },
+        {
+            code: String.raw`/[\q{ab|^}]/v`,
+            output: String.raw`/[\^\q{ab}]/v`,
+            errors: ["Unexpected string disjunction of single character."],
+        },
+        {
+            code: String.raw`/[\q{ab|c}&&\q{ab}]/v`,
+            output: String.raw`/[[c\q{ab}]&&\q{ab}]/v`,
+            errors: ["Unexpected string disjunction of single character."],
+        },
+        {
+            code: String.raw`/[A&&\q{&}]/v`,
+            output: String.raw`/[A&&\&]/v`,
+            errors: ["Unexpected string disjunction of single character."],
+        },
+        {
+            code: String.raw`/[A&&\q{^|ab}]/v`,
+            output: String.raw`/[A&&[\^\q{ab}]]/v`,
+            errors: ["Unexpected string disjunction of single character."],
+        },
+    ],
+})
diff --git a/tests/lib/rules/prefer-escape-replacement-dollar-char.ts b/tests/lib/rules/prefer-escape-replacement-dollar-char.ts
index c3bb752..e5dc182 100644
--- a/tests/lib/rules/prefer-escape-replacement-dollar-char.ts
+++ b/tests/lib/rules/prefer-escape-replacement-dollar-char.ts
@@ -19,6 +19,7 @@ tester.run("prefer-escape-replacement-dollar-char", rule as any, {
         `'abc'.replace(/./, "$\`$'");`,
         `'abc'.replace(/(.)/, '$1');`,
         `'abc'.replace(/(?<foo>.)/, '$<foo>');`,
+        String.raw`'€1,234'.replace(/[\q{€}]/v, '$$'); // "$1,234"`,
     ],
     invalid: [
         {
@@ -67,5 +68,11 @@ tester.run("prefer-escape-replacement-dollar-char", rule as any, {
                 "Unexpected replacement `$` character without escaping. Use `$$` instead.",
             ],
         },
+        {
+            code: String.raw`'€1,234'.replace(/[\q{€}]/v, '$'); // "$1,234"`,
+            errors: [
+                "Unexpected replacement `$` character without escaping. Use `$$` instead.",
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/prefer-regexp-exec.ts b/tests/lib/rules/prefer-regexp-exec.ts
index fdc2cc8..6adb281 100644
--- a/tests/lib/rules/prefer-regexp-exec.ts
+++ b/tests/lib/rules/prefer-regexp-exec.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/prefer-regexp-exec"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -19,6 +19,9 @@ tester.run("prefer-regexp-exec", rule as any, {
         const search = /thing/;
         search.exec(text);
         `,
+        `
+        /thin[[g]]/v.exec('something');
+        `,
     ],
     invalid: [
         {
@@ -72,5 +75,17 @@ tester.run("prefer-regexp-exec", rule as any, {
                 },
             ],
         },
+        {
+            code: `
+            'something'.match(/thin[[g]]/v);
+            `,
+            errors: [
+                {
+                    message: "Use the `RegExp#exec()` method instead.",
+                    line: 2,
+                    column: 13,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/prefer-regexp-test.ts b/tests/lib/rules/prefer-regexp-test.ts
index f3d466a..ff90882 100644
--- a/tests/lib/rules/prefer-regexp-test.ts
+++ b/tests/lib/rules/prefer-regexp-test.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/prefer-regexp-test"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -33,6 +33,11 @@ tester.run("prefer-regexp-test", rule as any, {
         const a = pattern.exec(text)
         const b = text.match(pattern)
         `,
+        `
+        const text = 'something';
+        const pattern = /thin[[g]]/v;
+        if (pattern.test(text)) {}
+        `,
     ],
     invalid: [
         {
@@ -186,5 +191,37 @@ tester.run("prefer-regexp-test", rule as any, {
                 "Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.",
             ],
         },
+        {
+            code: `
+            const text = 'something';
+            const pattern = /thin[[g]]/v;
+            if (pattern.exec(text)) {}
+            if (text.match(pattern)) {}
+            `,
+            output: `
+            const text = 'something';
+            const pattern = /thin[[g]]/v;
+            if (pattern.test(text)) {}
+            if (pattern.test(text)) {}
+            `,
+            errors: [
+                {
+                    message:
+                        "Use the `RegExp#test()` method instead of `RegExp#exec`, if you need a boolean.",
+                    line: 4,
+                    column: 25,
+                    endLine: 4,
+                    endColumn: 29,
+                },
+                {
+                    message:
+                        "Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.",
+                    line: 5,
+                    column: 17,
+                    endLine: 5,
+                    endColumn: 36,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/prefer-result-array-groups.ts b/tests/lib/rules/prefer-result-array-groups.ts
index b00d2bc..8d62b14 100644
--- a/tests/lib/rules/prefer-result-array-groups.ts
+++ b/tests/lib/rules/prefer-result-array-groups.ts
@@ -4,7 +4,7 @@ import rule from "../../../lib/rules/prefer-result-array-groups"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -51,6 +51,14 @@ tester.run("prefer-result-array-groups", rule as any, {
             // ...
         }
         `,
+        `
+        const regex = /reg[[exp]]/v
+        let match
+        while (match = regex.exec(foo)) {
+            const match = match[0]
+            // ...
+        }
+        `,
         // String.prototype.match()
         `
         const arr = "str".match(/regexp/)
@@ -231,6 +239,32 @@ tester.run("prefer-result-array-groups", rule as any, {
                 },
             ],
         },
+        {
+            code: `
+            const regex = /a(?<foo>[[b]])c/v
+            let match
+            while (match = regex.exec(foo)) {
+                const p1 = match[1]
+                // ...
+            }
+            `,
+            output: `
+            const regex = /a(?<foo>[[b]])c/v
+            let match
+            while (match = regex.exec(foo)) {
+                const p1 = match.groups.foo
+                // ...
+            }
+            `,
+            errors: [
+                {
+                    message:
+                        "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+                    line: 5,
+                    column: 28,
+                },
+            ],
+        },
         // String.prototype.match()
         {
             code: `