Repository: ota-meshi/eslint-plugin-regexp
Pull request #557 (2023-08-15 10:27:50)

Fix `isEqualNodes`
It doesn't work yet, because I need to update refa and RAA. I'll work on that today.

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: e406bea7f0ac7fd057585827904233f329f93817

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/ota-meshi/eslint-plugin-regexp/new/is-equal-nodes-fix?filename=.changeset/soft-dolphins-cheat.md&value=---%0A%22eslint-plugin-regexp%22%3A%20patch%0A---%0A%0AFix%20%60isEqualNodes%60%0A)



Patch:
diff --git a/.changeset/curvy-shoes-kiss.md b/.changeset/curvy-shoes-kiss.md
new file mode 100644
index 0000000..3a14b6f
--- /dev/null
+++ b/.changeset/curvy-shoes-kiss.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/prefer-d` rule
diff --git a/.changeset/early-guests-hug.md b/.changeset/early-guests-hug.md
new file mode 100644
index 0000000..c55d43f
--- /dev/null
+++ b/.changeset/early-guests-hug.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/prefer-unicode-codepoint-escapes` rule
diff --git a/.changeset/large-dingos-allow.md b/.changeset/large-dingos-allow.md
new file mode 100644
index 0000000..6918607
--- /dev/null
+++ b/.changeset/large-dingos-allow.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/unicode-escape` rule
diff --git a/.changeset/loud-flowers-search.md b/.changeset/loud-flowers-search.md
new file mode 100644
index 0000000..745d7b8
--- /dev/null
+++ b/.changeset/loud-flowers-search.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Improve `regexp/require-unicode-regexp` rule to allow patterns with v flag
diff --git a/.changeset/pre.json b/.changeset/pre.json
index c9df192..c1fa30f 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -8,12 +8,18 @@
     "brown-dragons-dance",
     "calm-onions-yell",
     "clever-jobs-clap",
+    "curvy-shoes-kiss",
     "cyan-waves-notice",
+    "heavy-rings-reply",
     "hungry-spoons-mix",
     "khaki-colts-tell",
+    "loud-flowers-search",
     "lovely-brooms-explode",
     "short-dingos-dance",
+    "six-squids-look",
     "small-doors-lie",
+    "smart-chefs-poke",
+    "tasty-penguins-cheat",
     "thin-ears-relax",
     "warm-ladybugs-wonder",
     "wicked-rules-rhyme"
diff --git a/.changeset/six-squids-look.md b/.changeset/six-squids-look.md
new file mode 100644
index 0000000..cbb4599
--- /dev/null
+++ b/.changeset/six-squids-look.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/no-useless-escape` rule
diff --git a/.changeset/smart-chefs-poke.md b/.changeset/smart-chefs-poke.md
new file mode 100644
index 0000000..aeabb6d
--- /dev/null
+++ b/.changeset/smart-chefs-poke.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/no-misleading-unicode-character` rule
diff --git a/.changeset/tasty-penguins-cheat.md b/.changeset/tasty-penguins-cheat.md
new file mode 100644
index 0000000..cc52915
--- /dev/null
+++ b/.changeset/tasty-penguins-cheat.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+`prefer-w`: Add support for `v` flag
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aac4fcb..4d5cf9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
 # eslint-plugin-regexp
 
+## 2.0.0-next.5
+
+### Minor Changes
+
+-   Add support for v flag to `regexp/prefer-d` rule ([#602](https://github.com/ota-meshi/eslint-plugin-regexp/pull/602))
+
+-   Add support for v flag to `regexp/negation` rule ([#560](https://github.com/ota-meshi/eslint-plugin-regexp/pull/560))
+
+-   Improve `regexp/require-unicode-regexp` rule to allow patterns with v flag ([#586](https://github.com/ota-meshi/eslint-plugin-regexp/pull/586))
+
+-   Add support for v flag to `regexp/no-useless-escape` rule ([#585](https://github.com/ota-meshi/eslint-plugin-regexp/pull/585))
+
+-   Add support for v flag to `regexp/no-misleading-unicode-character` rule ([#584](https://github.com/ota-meshi/eslint-plugin-regexp/pull/584))
+
+-   `prefer-w`: Add support for `v` flag ([#578](https://github.com/ota-meshi/eslint-plugin-regexp/pull/578))
+
 ## 2.0.0-next.4
 
 ### Minor Changes
diff --git a/docs/rules/prefer-d.md b/docs/rules/prefer-d.md
index 6e0f003..51f5cd2 100644
--- a/docs/rules/prefer-d.md
+++ b/docs/rules/prefer-d.md
@@ -53,6 +53,7 @@ var foo = /[^0-9]/;
 This option control how character class element equivalent to `\d` will be treated.
 
 *Note:* This option does not affect character classes equivalent to `\d`. E.g. `[\d]`, `[0-9]`, and `[0123456789]` are unaffected.
+It also does not affect expression non-nested operands equivalent to `\d`. E.g. `[\d&&x]`, and `[\d--x]` are unaffected.
 
 - `insideCharacterClass: "d"` (*default*)
 
@@ -88,6 +89,11 @@ This option control how character class element equivalent to `\d` will be treat
   /* ✗ BAD */
   var foo = /[\da-z]/;
   var foo = /[0-9]/;
+
+  /* Ignore */
+  var foo = /[\d--0]/v;
+  /* ✗ BAD */
+  var foo = /[[\da-z]--0]/v;
   ```
 
   </eslint-code-block>
@@ -107,6 +113,7 @@ This option control how character class element equivalent to `\d` will be treat
 
   /* ✗ BAD */
   var foo = /[0-9]/;
+  var foo = /[[0-9a-z]--0]/v;
   ```
 
   </eslint-code-block>
diff --git a/lib/rules/no-misleading-unicode-character.ts b/lib/rules/no-misleading-unicode-character.ts
index 136b659..80c5f57 100644
--- a/lib/rules/no-misleading-unicode-character.ts
+++ b/lib/rules/no-misleading-unicode-character.ts
@@ -35,7 +35,11 @@ function getProblem(grapheme: string, flags: ReadonlyFlags): Problem | null {
         (grapheme.length === 2 && !startsWithSurrogate(grapheme))
     ) {
         return "Multi"
-    } else if (!flags.unicode && startsWithSurrogate(grapheme)) {
+    } else if (
+        !flags.unicode &&
+        !flags.unicodeSets &&
+        startsWithSurrogate(grapheme)
+    ) {
         return "Surrogate"
     }
     return null
@@ -84,6 +88,13 @@ function getGraphemeProblems(
 ): GraphemeProblem[] {
     let offset = cc.negate ? 2 : 1
 
+    const ignoreElements = cc.elements.filter(
+        (element) =>
+            element.type === "CharacterClass" || // Nesting CharacterClass
+            element.type === "ExpressionCharacterClass" || // Nesting ExpressionCharacterClass
+            element.type === "ClassStringDisjunction",
+    )
+
     const graphemes = splitter.splitGraphemes(cc.raw.slice(offset, -1))
     const problems: GraphemeProblem[] = []
 
@@ -93,6 +104,14 @@ function getGraphemeProblems(
             const start = offset + cc.start
             const end = start + grapheme.length
 
+            if (
+                ignoreElements.some(
+                    (ignore) => ignore.start <= start && end <= ignore.end,
+                )
+            ) {
+                continue
+            }
+
             problems.push({
                 grapheme,
                 problem,
@@ -113,6 +132,7 @@ function getGraphemeProblems(
 function getGraphemeProblemsFix(
     problems: readonly GraphemeProblem[],
     cc: CharacterClass,
+    flags: ReadonlyFlags,
 ): string | null {
     if (cc.negate) {
         // we can't fix a negated character class
@@ -131,10 +151,7 @@ function getGraphemeProblemsFix(
     }
 
     // The prefix of graphemes
-    const prefix = problems
-        .map((p) => p.grapheme)
-        .sort((a, b) => b.length - a.length)
-        .join("|")
+    const prefixGraphemes = problems.map((p) => p.grapheme)
 
     // The rest of the character class
     let ccRaw = cc.raw
@@ -142,10 +159,17 @@ function getGraphemeProblemsFix(
         const { start, end } = problems[i]
         ccRaw = ccRaw.slice(0, start - cc.start) + ccRaw.slice(end - cc.start)
     }
+
+    if (flags.unicodeSets) {
+        const prefix = prefixGraphemes.join("|")
+        return `[\\q{${prefix}}${ccRaw.slice(1, -1)}]`
+    }
+
     if (ccRaw.startsWith("[^")) {
         ccRaw = `[\\${ccRaw.slice(1)}`
     }
 
+    const prefix = prefixGraphemes.sort((a, b) => b.length - a.length).join("|")
     let fix = prefix
     let singleAlternative = problems.length === 1
     if (ccRaw !== "[]") {
@@ -238,8 +262,7 @@ export default createRule("no-misleading-unicode-character", {
                         start: problems[0].start,
                         end: problems[problems.length - 1].end,
                     }
-
-                    const fix = getGraphemeProblemsFix(problems, ccNode)
+                    const fix = getGraphemeProblemsFix(problems, ccNode, flags)
 
                     const graphemes = problems
                         .map((p) => mention(p.grapheme))
diff --git a/lib/rules/no-useless-escape.ts b/lib/rules/no-useless-escape.ts
index 779c99f..b47a217 100644
--- a/lib/rules/no-useless-escape.ts
+++ b/lib/rules/no-useless-escape.ts
@@ -1,5 +1,9 @@
 import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
-import type { Character } from "@eslint-community/regexpp/ast"
+import type {
+    Character,
+    CharacterClass,
+    ExpressionCharacterClass,
+} from "@eslint-community/regexpp/ast"
 import type { RegExpContext } from "../utils"
 import {
     createRule,
@@ -21,6 +25,19 @@ import {
     CP_PIPE,
     CP_MINUS,
     canUnwrapped,
+    CP_HASH,
+    CP_PERCENT,
+    CP_BAN,
+    CP_AMP,
+    CP_COMMA,
+    CP_COLON,
+    CP_SEMI,
+    CP_LT,
+    CP_EQ,
+    CP_GT,
+    CP_AT,
+    CP_TILDE,
+    CP_BACKTICK,
 } from "../utils"
 
 const REGEX_CHAR_CLASS_ESCAPES = new Set([
@@ -28,6 +45,18 @@ const REGEX_CHAR_CLASS_ESCAPES = new Set([
     CP_CLOSING_BRACKET, // ]
     CP_MINUS, // -
 ])
+const REGEX_CLASS_SET_CHAR_CLASS_ESCAPE = new Set([
+    CP_BACK_SLASH, // \\
+    CP_SLASH, // /
+    CP_OPENING_BRACKET, // [
+    CP_CLOSING_BRACKET, // ]
+    CP_OPENING_BRACE, // {
+    CP_CLOSING_BRACE, // }
+    CP_PIPE, // |
+    CP_OPENING_PAREN, // (
+    CP_CLOSING_PAREN, // )
+    CP_MINUS, // -,
+])
 const REGEX_ESCAPES = new Set([
     CP_BACK_SLASH, // \\
     CP_SLASH, // /
@@ -47,6 +76,33 @@ const REGEX_ESCAPES = new Set([
 ])
 
 const POTENTIAL_ESCAPE_SEQUENCE = new Set("uxkpP")
+const POTENTIAL_ESCAPE_SEQUENCE_FOR_CHAR_CLASS = new Set([
+    ...POTENTIAL_ESCAPE_SEQUENCE,
+    "q",
+])
+// A single character set of ClassSetReservedDoublePunctuator.
+// && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~ are ClassSetReservedDoublePunctuator
+const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR = new Set([
+    CP_BAN, // !
+    CP_HASH, // #
+    CP_DOLLAR, // $
+    CP_PERCENT, // %
+    CP_AMP, // &
+    CP_STAR, // *
+    CP_PLUS, // +
+    CP_COMMA, // ,
+    CP_DOT, // .
+    CP_COLON, // :
+    CP_SEMI, // ;
+    CP_LT, // <
+    CP_EQ, // =
+    CP_GT, // >
+    CP_QUESTION, // ?
+    CP_AT, // @
+    CP_CARET, // ^
+    CP_BACKTICK, // `
+    CP_TILDE, // ~
+])
 
 export default createRule("no-useless-escape", {
     meta: {
@@ -65,6 +121,8 @@ export default createRule("no-useless-escape", {
     create(context) {
         function createVisitor({
             node,
+            flags,
+            pattern,
             getRegexpLocation,
             fixReplaceNode,
         }: RegExpContext): RegExpVisitor.Handlers {
@@ -85,37 +143,85 @@ export default createRule("no-useless-escape", {
                 })
             }
 
-            let inCharacterClass = false
+            const characterClassStack: (
+                | CharacterClass
+                | ExpressionCharacterClass
+            )[] = []
             return {
-                onCharacterClassEnter() {
-                    inCharacterClass = true
-                },
-                onCharacterClassLeave() {
-                    inCharacterClass = false
-                },
+                onCharacterClassEnter: (characterClassNode) =>
+                    characterClassStack.unshift(characterClassNode),
+                onCharacterClassLeave: () => characterClassStack.shift(),
+                onExpressionCharacterClassEnter: (characterClassNode) =>
+                    characterClassStack.unshift(characterClassNode),
+                onExpressionCharacterClassLeave: () =>
+                    characterClassStack.shift(),
                 onCharacterEnter(cNode) {
                     if (cNode.raw.startsWith("\\")) {
                         // escapes
                         const char = cNode.raw.slice(1)
-                        if (char === String.fromCodePoint(cNode.value)) {
-                            const allowedEscapes = inCharacterClass
-                                ? REGEX_CHAR_CLASS_ESCAPES
-                                : REGEX_ESCAPES
+                        const escapedChar = String.fromCodePoint(cNode.value)
+                        if (char === escapedChar) {
+                            let allowedEscapes: Set<number>
+                            if (characterClassStack.length) {
+                                allowedEscapes = flags.unicodeSets
+                                    ? REGEX_CLASS_SET_CHAR_CLASS_ESCAPE
+                                    : REGEX_CHAR_CLASS_ESCAPES
+                            } else {
+                                allowedEscapes = REGEX_ESCAPES
+                            }
                             if (allowedEscapes.has(cNode.value)) {
                                 return
                             }
-                            if (inCharacterClass && cNode.value === CP_CARET) {
-                                const target =
-                                    cNode.parent.type === "CharacterClassRange"
-                                        ? cNode.parent
-                                        : cNode
-                                const parent = target.parent
-                                if (parent.type === "CharacterClass") {
-                                    if (parent.elements.indexOf(target) === 0) {
+                            if (characterClassStack.length) {
+                                const characterClassNode =
+                                    characterClassStack[0]
+                                if (cNode.value === CP_CARET) {
+                                    if (
+                                        characterClassNode.start + 1 ===
+                                        cNode.start
+                                    ) {
                                         // e.g. /[\^]/
                                         return
                                     }
                                 }
+                                if (flags.unicodeSets) {
+                                    if (
+                                        REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR.has(
+                                            cNode.value,
+                                        )
+                                    ) {
+                                        if (
+                                            pattern[cNode.end] === escapedChar
+                                        ) {
+                                            // Escaping is valid if it is a ClassSetReservedDoublePunctuator.
+                                            return
+                                        }
+                                        const prevIndex = cNode.start - 1
+                                        if (
+                                            pattern[prevIndex] === escapedChar
+                                        ) {
+                                            if (escapedChar !== "^") {
+                                                // e.g. [&\&]
+                                                //        ^ // If it's the second character, it's a valid escape.
+                                                return
+                                            }
+                                            const elementStartIndex =
+                                                characterClassNode.start +
+                                                1 + // opening bracket(`[`)
+                                                (characterClassNode.negate
+                                                    ? 1 // `negate` caret(`^`)
+                                                    : 0)
+                                            if (
+                                                elementStartIndex <= prevIndex
+                                            ) {
+                                                // [^^\^], [_^\^]
+                                                //    ^       ^ // If it's the second caret(`^`) character, it's a valid escape.
+                                                // But [^\^] is unnecessary escape.
+                                                return
+                                            }
+                                        }
+                                    }
+                                }
                             }
                             if (!canUnwrapped(cNode, char)) {
                                 return
@@ -124,7 +230,11 @@ export default createRule("no-useless-escape", {
                                 cNode,
                                 0,
                                 char,
-                                !POTENTIAL_ESCAPE_SEQUENCE.has(char),
+                                !(
+                                    characterClassStack.length
+                                        ? POTENTIAL_ESCAPE_SEQUENCE_FOR_CHAR_CLASS
+                                        : POTENTIAL_ESCAPE_SEQUENCE
+                                ).has(char),
                             )
                         }
                     }
diff --git a/lib/rules/prefer-d.ts b/lib/rules/prefer-d.ts
index 464e896..23a9ed5 100644
--- a/lib/rules/prefer-d.ts
+++ b/lib/rules/prefer-d.ts
@@ -6,12 +6,14 @@ import {
     CP_DIGIT_ZERO,
     CP_DIGIT_NINE,
 } from "../utils"
-import { Chars, toCharSet } from "regexp-ast-analysis"
+import { Chars, toUnicodeSet } from "regexp-ast-analysis"
 import { mention } from "../utils/mention"
 import type {
+    CharacterClass,
     CharacterClassElement,
     CharacterClassRange,
     EscapeCharacterSet,
+    ExpressionCharacterClass,
 } from "@eslint-community/regexpp/ast"
 
 /**
@@ -66,61 +68,66 @@ export default createRule("prefer-d", {
             getRegexpLocation,
             fixReplaceNode,
         }: RegExpContext): RegExpVisitor.Handlers {
-            return {
-                onCharacterClassEnter(ccNode) {
-                    // FIXME: TS Error
-                    // @ts-expect-error -- FIXME
-                    const charSet = toCharSet(ccNode, flags)
+            function verifyCharacterClass(
+                ccNode: CharacterClass | ExpressionCharacterClass,
+            ) {
+                const charSet = toUnicodeSet(ccNode, flags)
 
-                    let predefined: string | undefined = undefined
-                    if (charSet.equals(Chars.digit(flags))) {
-                        predefined = "\\d"
-                    } else if (charSet.equals(Chars.digit(flags).negate())) {
-                        predefined = "\\D"
-                    }
+                let predefined: string | undefined = undefined
+                if (charSet.equals(Chars.digit(flags))) {
+                    predefined = "\\d"
+                } else if (charSet.equals(Chars.digit(flags).negate())) {
+                    predefined = "\\D"
+                }
+
+                if (predefined) {
+                    context.report({
+                        node,
+                        loc: getRegexpLocation(ccNode),
+                        messageId: "unexpected",
+                        data: {
+                            type: "character class",
+                            expr: mention(ccNode),
+                            instead: predefined,
+                        },
+                        fix: fixReplaceNode(ccNode, predefined),
+                    })
+                    return
+                }
 
-                    if (predefined) {
+                if (
+                    insideCharacterClass === "ignore" ||
+                    ccNode.type !== "CharacterClass"
+                ) {
+                    return
+                }
+
+                const expected = insideCharacterClass === "d" ? "\\d" : "0-9"
+
+                // check the elements in this character class
+                for (const e of ccNode.elements) {
+                    if (isDigits(e) && e.raw !== expected) {
                         context.report({
                             node,
-                            loc: getRegexpLocation(ccNode),
+                            loc: getRegexpLocation(e),
                             messageId: "unexpected",
                             data: {
-                                type: "character class",
-                                expr: mention(ccNode),
-                                instead: predefined,
+                                type:
+                                    e.type === "CharacterSet"
+                                        ? "character set"
+                                        : "character class range",
+                                expr: mention(e),
+                                instead: expected,
                             },
-                            fix: fixReplaceNode(ccNode, predefined),
+                            fix: fixReplaceNode(e, expected),
                         })
-                        return
-                    }
-
-                    if (insideCharacterClass === "ignore") {
-                        return
                     }
+                }
+            }
 
-                    const expected =
-                        insideCharacterClass === "d" ? "\\d" : "0-9"
-
-                    // check the elements in this character class
-                    for (const e of ccNode.elements) {
-                        if (isDigits(e) && e.raw !== expected) {
-                            context.report({
-                                node,
-                                loc: getRegexpLocation(e),
-                                messageId: "unexpected",
-                                data: {
-                                    type:
-                                        e.type === "CharacterSet"
-                                            ? "character set"
-                                            : "character class range",
-                                    expr: mention(e),
-                                    instead: expected,
-                                },
-                                fix: fixReplaceNode(e, expected),
-                            })
-                        }
-                    }
-                },
+            return {
+                onCharacterClassEnter: verifyCharacterClass,
+                onExpressionCharacterClassEnter: verifyCharacterClass,
             }
         }
 
diff --git a/lib/rules/prefer-unicode-codepoint-escapes.ts b/lib/rules/prefer-unicode-codepoint-escapes.ts
index c26f209..777d221 100644
--- a/lib/rules/prefer-unicode-codepoint-escapes.ts
+++ b/lib/rules/prefer-unicode-codepoint-escapes.ts
@@ -23,7 +23,7 @@ export default createRule("prefer-unicode-codepoint-escapes", {
         ): RegExpVisitor.Handlers {
             const { node, flags, getRegexpLocation, fixReplaceNode } =
                 regexpContext
-            if (!flags.unicode) {
+            if (!flags.unicode && !flags.unicodeSets) {
                 return {}
             }
             return {
diff --git a/lib/rules/prefer-w.ts b/lib/rules/prefer-w.ts
index 6db0c70..61097e3 100644
--- a/lib/rules/prefer-w.ts
+++ b/lib/rules/prefer-w.ts
@@ -16,7 +16,7 @@ import {
     CP_DIGIT_NINE,
     CP_LOW_LINE,
 } from "../utils"
-import { Chars, toCharSet } from "regexp-ast-analysis"
+import { Chars, toUnicodeSet } from "regexp-ast-analysis"
 import { mention } from "../utils/mention"
 
 function isSmallLetterRange(node: CharacterClassElement) {
@@ -40,7 +40,7 @@ function isDigitRangeOrSet(node: CharacterClassElement) {
         (node.type === "CharacterClassRange" &&
             node.min.value === CP_DIGIT_ZERO &&
             node.max.value === CP_DIGIT_NINE) ||
-        (node.type === "CharacterSet" && node.kind === "digit")
+        (node.type === "CharacterSet" && node.kind === "digit" && !node.negate)
     )
 }
 
@@ -73,14 +73,13 @@ export default createRule("prefer-w", {
         }: RegExpContext): RegExpVisitor.Handlers {
             return {
                 onCharacterClassEnter(ccNode: CharacterClass) {
-                    // FIXME: TS Error
-                    // @ts-expect-error -- FIXME
-                    const charSet = toCharSet(ccNode, flags)
+                    const charSet = toUnicodeSet(ccNode, flags)
 
                     let predefined: string | undefined = undefined
-                    if (charSet.equals(Chars.word(flags))) {
+                    const word = Chars.word(flags)
+                    if (charSet.equals(word)) {
                         predefined = "\\w"
-                    } else if (charSet.equals(Chars.word(flags).negate())) {
+                    } else if (charSet.equals(word.negate())) {
                         predefined = "\\W"
                     }
 
diff --git a/lib/rules/require-unicode-regexp.ts b/lib/rules/require-unicode-regexp.ts
index 5f7e7db..18505a5 100644
--- a/lib/rules/require-unicode-regexp.ts
+++ b/lib/rules/require-unicode-regexp.ts
@@ -15,8 +15,8 @@ import type { ReadonlyFlags } from "regexp-ast-analysis"
 import {
     hasSomeDescendant,
     toCache,
-    toCharSet,
     getFirstCharAfter,
+    toUnicodeSet,
 } from "regexp-ast-analysis"
 
 const UTF16_MAX = 0xffff
@@ -146,9 +146,7 @@ function isCompatibleCharLike(
     flags: ReadonlyFlags,
     uFlags: ReadonlyFlags,
 ): boolean {
-    // FIXME: TS Error
-    // @ts-expect-error -- FIXME
-    const cs = toCharSet(char, flags)
+    const cs = toUnicodeSet(char, flags)
     if (!cs.isDisjointWith(SURROGATES)) {
         // If the character (class/set) contains high or low
         // surrogates, then we won't be able to guarantee that the
@@ -156,12 +154,10 @@ function isCompatibleCharLike(
         return false
     }
 
-    // FIXME: TS Error
-    // @ts-expect-error -- FIXME
-    const uCs = toCharSet(char, uFlags)
+    const uCs = toUnicodeSet(char, uFlags)
 
     // Compare the ranges.
-    return rangeEqual(cs.ranges, uCs.ranges)
+    return rangeEqual(cs.chars.ranges, uCs.chars.ranges)
 }
 
 /**
@@ -203,23 +199,19 @@ function isCompatibleQuantifier(
         return undefined
     }
 
-    // FIXME: TS Error
-    // @ts-expect-error -- FIXME
-    const cs = toCharSet(q.element, flags)
+    const cs = toUnicodeSet(q.element, flags)
     if (!cs.isSupersetOf(SURROGATES)) {
         // failed condition 1
         return false
     }
 
-    // FIXME: TS Error
-    // @ts-expect-error -- FIXME
-    const uCs = toCharSet(q.element, uFlags)
+    const uCs = toUnicodeSet(q.element, uFlags)
     if (!uCs.isSupersetOf(SURROGATES) || !uCs.isSupersetOf(ASTRAL)) {
         // failed condition 2
         return false
     }
 
-    if (!rangeEqual(cs.ranges, uCs.without(ASTRAL).ranges)) {
+    if (!rangeEqual(cs.chars.ranges, uCs.without(ASTRAL).chars.ranges)) {
         // failed condition 3
         return false
     }
@@ -353,7 +345,7 @@ export default createRule("require-unicode-regexp", {
                 return {}
             }
 
-            if (!flags.unicode) {
+            if (!flags.unicode && !flags.unicodeSets) {
                 context.report({
                     node,
                     loc: getFlagsLocation(),
diff --git a/lib/rules/unicode-escape.ts b/lib/rules/unicode-escape.ts
index adf8389..49455d9 100644
--- a/lib/rules/unicode-escape.ts
+++ b/lib/rules/unicode-escape.ts
@@ -88,7 +88,7 @@ export default createRule("unicode-escape", {
             regexpContext: RegExpContext,
         ): RegExpVisitor.Handlers {
             const { flags } = regexpContext
-            if (!flags.unicode) {
+            if (!flags.unicode && !flags.unicodeSets) {
                 return {}
             }
             return {
diff --git a/lib/utils/regexp-ast/is-covered.ts b/lib/utils/regexp-ast/is-covered.ts
index b5eee73..3abc23e 100644
--- a/lib/utils/regexp-ast/is-covered.ts
+++ b/lib/utils/regexp-ast/is-covered.ts
@@ -8,8 +8,12 @@ import type {
     LookaroundAssertion,
 } from "@eslint-community/regexpp/ast"
 import { isEqualNodes } from "./is-equals"
-import type { ReadonlyFlags, ToCharSetElement } from "regexp-ast-analysis"
-import { toCharSet } from "regexp-ast-analysis"
+import type {
+    ReadonlyFlags,
+    ToCharSetElement,
+    ToUnicodeSetElement,
+} from "regexp-ast-analysis"
+import { toCharSet, toUnicodeSet } from "regexp-ast-analysis"
 import type { CharSet } from "refa"
 
 type Options = {
@@ -54,6 +58,10 @@ class NormalizedCharacter implements NormalizedNodeBase {
         return new NormalizedCharacter(toCharSet(element, options.flags))
     }
 
+    public static fromChars(charSet: CharSet) {
+        return new NormalizedCharacter(charSet)
+    }
+
     private constructor(charSet: CharSet) {
         this.charSet = charSet
     }
@@ -69,7 +77,7 @@ class NormalizedAlternative implements NormalizedNodeBase {
 
     public readonly raw: string
 
-    public readonly elements: NormalizedNode[]
+    public readonly elements: readonly NormalizedNode[]
 
     public static fromAlternative(node: Alternative, options: Options) {
         const normalizeElements = [
@@ -107,7 +115,7 @@ class NormalizedAlternative implements NormalizedNodeBase {
 
     public static fromElements(
         elements: NormalizedNode[],
-        node: Alternative | Quantifier,
+        node: Alternative | Quantifier | ToUnicodeSetElement,
     ) {
         const normalizeElements = [
             ...NormalizedAlternative.normalizedElements(function* () {
@@ -131,7 +139,7 @@ class NormalizedAlternative implements NormalizedNodeBase {
 
     private constructor(
         elements: NormalizedNode[],
-        node: Alternative | Quantifier,
+        node: Alternative | Quantifier | ToUnicodeSetElement,
     ) {
         this.raw = node.raw
         this.elements = elements
@@ -148,11 +156,9 @@ class NormalizedDisjunctions implements NormalizedNodeBase {
 
     public readonly raw: string
 
-    public readonly node: CapturingGroup | Group | Pattern
+    private readonly getAlternatives: () => readonly NormalizedAlternative[]
 
-    private readonly options: Options
-
-    public normalizedAlternatives?: NormalizedAlternative[]
+    private normalizedAlternatives?: readonly NormalizedAlternative[]
 
     public static fromNode(
         node: CapturingGroup | Group | Pattern,
@@ -164,32 +170,35 @@ class NormalizedDisjunctions implements NormalizedNodeBase {
                 options,
             )
         }
-        return new NormalizedDisjunctions(node, options)
+        return new NormalizedDisjunctions(node, () => {
+            return node.alternatives.map((alt) => {
+                const n = normalizeNode(alt, options)
+                if (n.type === "NormalizedAlternative") {
+                    return n
+                }
+                return NormalizedAlternative.fromElements([n], alt)
+            })
+        })
+    }
+
+    public static fromAlternatives(
+        alternatives: readonly NormalizedAlternative[],
+        node: CapturingGroup | Group | Pattern | ToUnicodeSetElement,
+    ) {
+        return new NormalizedDisjunctions(node, () => alternatives)
     }
 
     private constructor(
-        node: CapturingGroup | Group | Pattern,
-        options: Options,
+        node: CapturingGroup | Group | Pattern | ToUnicodeSetElement,
+        getAlternatives: () => readonly NormalizedAlternative[],
     ) {
         this.raw = node.raw
-        this.node = node
-        this.options = options
+        this.getAlternatives = getAlternatives
     }
 
-    public get alternatives() {
-        if (this.normalizedAlternatives) {
-            return this.normalizedAlternatives
-        }
-        this.normalizedAlternatives = []
-        for (const alt of this.node.alternatives) {
-            const node = normalizeNode(alt, this.options)
-            if (node.type === "NormalizedAlternative") {
-                this.normalizedAlternatives.push(node)
-            } else {
-                this.normalizedAlternatives.push(
-                    NormalizedAlternative.fromElements([node], alt),
-                )
-            }
+    public get alternatives(): readonly NormalizedAlternative[] {
+        if (!this.normalizedAlternatives) {
+            this.normalizedAlternatives = this.getAlternatives()
         }
         return this.normalizedAlternatives
     }
@@ -208,7 +217,7 @@ class NormalizedLookaroundAssertion implements NormalizedNodeBase {
 
     private readonly options: Options
 
-    public normalizedAlternatives?: NormalizedAlternative[]
+    private normalizedAlternatives?: NormalizedAlternative[]
 
     public static fromNode(node: LookaroundAssertion, options: Options) {
         return new NormalizedLookaroundAssertion(node, options)
@@ -220,7 +229,7 @@ class NormalizedLookaroundAssertion implements NormalizedNodeBase {
         this.options = options
     }
 
-    public get alternatives() {
+    public get alternatives(): readonly NormalizedAlternative[] {
         if (this.normalizedAlternatives) {
             return this.normalizedAlternatives
         }
@@ -425,44 +434,66 @@ function normalizeNodeWithoutCache(
     node: Node,
     options: Options,
 ): NormalizedNode {
-    if (
-        node.type === "CharacterSet" ||
-        node.type === "CharacterClass" ||
-        node.type === "Character" ||
-        node.type === "CharacterClassRange"
-    ) {
-        // FIXME: TS Error
-        // @ts-expect-error -- FIXME
-        return NormalizedCharacter.fromElement(node, options)
-    }
-    if (node.type === "Alternative") {
-        return NormalizedAlternative.fromAlternative(node, options)
-    }
-    if (node.type === "Quantifier") {
-        return NormalizedOptional.fromQuantifier(node, options)
-    }
-    if (
-        node.type === "CapturingGroup" ||
-        node.type === "Group" ||
-        node.type === "Pattern"
-    ) {
-        return NormalizedDisjunctions.fromNode(node, options)
-    }
-    if (node.type === "RegExpLiteral") {
-        return normalizeNode(node.pattern, options)
-    }
-    if (node.type === "Assertion") {
-        if (node.kind === "lookahead" || node.kind === "lookbehind") {
-            return NormalizedLookaroundAssertion.fromNode(node, options)
+    switch (node.type) {
+        case "CharacterSet":
+        case "CharacterClass":
+        case "Character":
+        case "CharacterClassRange":
+        case "ExpressionCharacterClass":
+        case "ClassIntersection":
+        case "ClassSubtraction":
+        case "ClassStringDisjunction":
+        case "StringAlternative": {
+            const set = toUnicodeSet(node, options.flags)
+            if (set.accept.isEmpty) {
+                return NormalizedCharacter.fromChars(set.chars)
+            }
+
+            const alternatives = set.wordSets.map((wordSet) => {
+                return NormalizedAlternative.fromElements(
+                    wordSet.map(NormalizedCharacter.fromChars),
+                    node,
+                )
+            })
+            return NormalizedDisjunctions.fromAlternatives(alternatives, node)
         }
-        return NormalizedOther.fromNode(node)
+
+        case "Alternative":
+            return NormalizedAlternative.fromAlternative(node, options)
+
+        case "Quantifier":
+            return NormalizedOptional.fromQuantifier(node, options)
+
+        case "CapturingGroup":
+        case "Group":
+        case "Pattern":
+            return NormalizedDisjunctions.fromNode(node, options)
+
+        case "Assertion":
+            if (node.kind === "lookahead" || node.kind === "lookbehind") {
+                return NormalizedLookaroundAssertion.fromNode(node, options)
+            }
+            return NormalizedOther.fromNode(node)
+
+        case "RegExpLiteral":
+            return normalizeNode(node.pattern, options)
+
+        case "Backreference":
+        case "Flags":
+            return NormalizedOther.fromNode(node)
+
+        default:
+            return assertNever(node)
     }
-    return NormalizedOther.fromNode(node)
+}
+
+function assertNever(value: never): never {
+    throw new Error(`Invalid value: ${value}`)
 }
 
 /** Check whether the right node is covered by the left nodes. */
 function isCoveredAnyNode(
-    left: NormalizedNode[],
+    left: readonly NormalizedNode[],
     right: NormalizedNode,
     options: Options,
 ) {
@@ -476,8 +507,8 @@ function isCoveredAnyNode(
 
 /** Check whether the right nodes is covered by the left nodes. */
 function isCoveredAltNodes(
-    leftNodes: NormalizedNode[],
-    rightNodes: NormalizedNode[],
+    leftNodes: readonly NormalizedNode[],
+    rightNodes: readonly NormalizedNode[],
     options: Options,
 ): boolean {
     const left = options.canOmitRight ? omitEnds(leftNodes) : [...leftNodes]
@@ -561,7 +592,7 @@ function isCoveredAltNodes(
 /**
  * Exclude the end optionals.
  */
-function omitEnds(nodes: NormalizedNode[]): NormalizedNode[] {
+function omitEnds(nodes: readonly NormalizedNode[]): NormalizedNode[] {
     for (let index = nodes.length - 1; index >= 0; index--) {
         const node = nodes[index]
         if (node.type !== "NormalizedOptional") {
diff --git a/lib/utils/regexp-ast/is-equals.ts b/lib/utils/regexp-ast/is-equals.ts
index c13fda0..4d22b2f 100644
--- a/lib/utils/regexp-ast/is-equals.ts
+++ b/lib/utils/regexp-ast/is-equals.ts
@@ -4,24 +4,7 @@ import type {
     ToUnicodeSetElement,
 } from "regexp-ast-analysis"
 import { toUnicodeSet } from "regexp-ast-analysis"
-import type {
-    Alternative,
-    Assertion,
-    CapturingGroup,
-    Quantifier,
-    Group,
-    CharacterClass,
-    CharacterSet,
-    Character,
-    Backreference,
-    CharacterClassRange,
-    Node,
-    RegExpLiteral,
-    Pattern,
-    Flags,
-    Element,
-    CharacterClassElement,
-} from "@eslint-community/regexpp/ast"
+import type { Node } from "@eslint-community/regexpp/ast"
 import type { ShortCircuit } from "./common"
 
 /**
@@ -63,21 +46,19 @@ function isEqualChar(
     return toUnicodeSet(a, flags).equals(toUnicodeSet(b, flags))
 }
 
-const EQUALS_CHECKER = {
-    Alternative(
-        a: Alternative,
-        b: Alternative,
+type OfType<T extends Node["type"]> = Extract<Node, { type: T }>
+const EQUALS_CHECKER: {
+    [T in Node["type"]]: (
+        a: OfType<T>,
+        b: OfType<T>,
         flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
-        return isEqualElements(a.elements, b.elements, flags, shortCircuit)
+        shortCircuit: ShortCircuit | undefined,
+    ) => boolean
+} = {
+    Alternative(a, b, flags, shortCircuit) {
+        return isEqualConcatenation(a.elements, b.elements, flags, shortCircuit)
     },
-    Assertion(
-        a: Assertion,
-        b: Assertion,
-        flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
+    Assertion(a, b, flags, shortCircuit) {
         if (a.kind === "start" || a.kind === "end") {
             /* istanbul ignore next */
             return a.kind === b.kind
@@ -87,7 +68,7 @@ const EQUALS_CHECKER = {
         }
         if (a.kind === "lookahead" || a.kind === "lookbehind") {
             if (b.kind === a.kind && a.negate === b.negate) {
-                return isEqualAlternatives(
+                return isEqualSet(
                     a.alternatives,
                     b.alternatives,
                     flags,
@@ -99,43 +80,52 @@ const EQUALS_CHECKER = {
         /* istanbul ignore next */
         return false
     },
-    Backreference(a: Backreference, b: Backreference) {
+    Backreference(a, b) {
         return a.ref === b.ref
     },
-    CapturingGroup(
-        a: CapturingGroup,
-        b: CapturingGroup,
-        flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
+    CapturingGroup(a, b, flags, shortCircuit) {
         return (
             a.name === b.name &&
-            isEqualAlternatives(
-                a.alternatives,
-                b.alternatives,
-                flags,
-                shortCircuit,
-            )
+            isEqualSet(a.alternatives, b.alternatives, flags, shortCircuit)
         )
     },
-    Character(a: Character, b: Character, flags: ReadonlyFlags) {
+    Character(a, b, flags) {
         return isEqualChar(a, b, flags)
     },
-    CharacterClass(a: CharacterClass, b: CharacterClass, flags: ReadonlyFlags) {
+    CharacterClass(a, b, flags) {
         return isEqualChar(a, b, flags)
     },
-    CharacterClassRange(
-        a: CharacterClassRange,
-        b: CharacterClassRange,
-        flags: ReadonlyFlags,
-    ) {
+    CharacterClassRange(a, b, flags) {
         return isEqualChar(a, b, flags)
     },
-    CharacterSet(a: CharacterSet, b: CharacterSet, flags: ReadonlyFlags) {
+    CharacterSet(a, b, flags) {
         return isEqualChar(a, b, flags)
     },
+    ClassIntersection(a, b, flags, shortCircuit) {
+        return isEqualSet(
+            [a.left, a.right],
+            [b.left, b.right],
+            flags,
+            shortCircuit,
+        )
+    },
+    ClassStringDisjunction(a, b, flags, shortCircuit) {
+        return isEqualSet(a.alternatives, b.alternatives, flags, shortCircuit)
+    },
+    ClassSubtraction(a, b, flags, shortCircuit) {
+        return (
+            isEqualNodes(a.left, b.left, flags, shortCircuit) &&
+            isEqualNodes(a.right, b.right, flags, shortCircuit)
+        )
+    },
+    ExpressionCharacterClass(a, b, flags) {
+        return (
+            a.negate === b.negate &&
+            isEqualNodes(a.expression, b.expression, flags)
+        )
+    },
     /* istanbul ignore next */
-    Flags(a: Flags, b: Flags) {
+    Flags(a, b) {
         /* istanbul ignore next */
         return (
             a.dotAll === b.dotAll &&
@@ -148,38 +138,13 @@ const EQUALS_CHECKER = {
             a.unicodeSets === b.unicodeSets
         )
     },
-    Group(
-        a: Group,
-        b: Group,
-        flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
-        return isEqualAlternatives(
-            a.alternatives,
-            b.alternatives,
-            flags,
-            shortCircuit,
-        )
+    Group(a, b, flags, shortCircuit) {
+        return isEqualSet(a.alternatives, b.alternatives, flags, shortCircuit)
     },
-    Pattern(
-        a: Pattern,
-        b: Pattern,
-        flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
-        return isEqualAlternatives(
-            a.alternatives,
-            b.alternatives,
-            flags,
-            shortCircuit,
-        )
+    Pattern(a, b, flags, shortCircuit) {
+        return isEqualSet(a.alternatives, b.alternatives, flags, shortCircuit)
     },
-    Quantifier(
-        a: Quantifier,
-        b: Quantifier,
-        flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
+    Quantifier(a, b, flags, shortCircuit) {
         return (
             a.min === b.min &&
             a.max === b.max &&
@@ -187,17 +152,15 @@ const EQUALS_CHECKER = {
             isEqualNodes(a.element, b.element, flags, shortCircuit)
         )
     },
-    RegExpLiteral(
-        a: RegExpLiteral,
-        b: RegExpLiteral,
-        flags: ReadonlyFlags,
-        shortCircuit?: ShortCircuit,
-    ) {
+    RegExpLiteral(a, b, flags, shortCircuit) {
         return (
             isEqualNodes(a.pattern, b.pattern, flags, shortCircuit) &&
             isEqualNodes(a.flags, b.flags, flags, shortCircuit)
         )
     },
+    StringAlternative(a, b, flags, shortCircuit) {
+        return isEqualConcatenation(a.elements, b.elements, flags, shortCircuit)
+    },
 }
 
 /**
@@ -233,8 +196,6 @@ export function isEqualNodes<N extends Node>(
         }
     }
     if (/[(*+?[\\{|]/u.test(a.raw) || /[(*+?[\\{|]/u.test(b.raw)) {
-        // FIXME: TS Error
-        // @ts-expect-error -- FIXME
         return EQUALS_CHECKER[a.type](
             a as never,
             b as never,
@@ -246,9 +207,9 @@ export function isEqualNodes<N extends Node>(
 }
 
 /** Check whether given elements are equals or not. */
-function isEqualElements(
-    a: Element[],
-    b: Element[],
+function isEqualConcatenation<N extends Node>(
+    a: readonly N[],
+    b: readonly N[],
     flags: ReadonlyFlags,
     shortCircuit?: ShortCircuit,
 ) {
@@ -266,9 +227,9 @@ function isEqualElements(
 }
 
 /** Check whether given alternatives are equals or not. */
-function isEqualAlternatives<N extends Alternative | CharacterClassElement>(
-    a: N[],
-    b: N[],
+function isEqualSet<N extends Node>(
+    a: readonly N[],
+    b: readonly N[],
     flags: ReadonlyFlags,
     shortCircuit?: ShortCircuit,
 ) {
diff --git a/lib/utils/unicode.ts b/lib/utils/unicode.ts
index 9ba1443..91c1467 100644
--- a/lib/utils/unicode.ts
+++ b/lib/utils/unicode.ts
@@ -8,15 +8,23 @@ export const CP_FF = 12
 export const CP_CR = 13
 export const CP_SPACE = " ".codePointAt(0)!
 export const CP_BAN = "!".codePointAt(0)!
+export const CP_HASH = "#".codePointAt(0)
 export const CP_DOLLAR = "$".codePointAt(0)!
+export const CP_PERCENT = "%".codePointAt(0)!
+export const CP_AMP = "&".codePointAt(0)!
 export const CP_OPENING_PAREN = "(".codePointAt(0)!
 export const CP_CLOSING_PAREN = ")".codePointAt(0)!
 export const CP_STAR = "*".codePointAt(0)!
 export const CP_PLUS = "+".codePointAt(0)!
+export const CP_COMMA = ",".codePointAt(0)!
 export const CP_MINUS = "-".codePointAt(0)!
 export const CP_DOT = ".".codePointAt(0)!
 export const CP_SLASH = "/".codePointAt(0)!
 export const CP_COLON = ":".codePointAt(0)!
+export const CP_SEMI = ";".codePointAt(0)!
+export const CP_LT = "<".codePointAt(0)!
+export const CP_EQ = "=".codePointAt(0)!
+export const CP_GT = ">".codePointAt(0)!
 export const CP_QUESTION = "?".codePointAt(0)!
 export const CP_AT = "@".codePointAt(0)!
 export const CP_OPENING_BRACKET = "[".codePointAt(0)!
diff --git a/package.json b/package.json
index 8ffebdc..e4e751a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "eslint-plugin-regexp",
-    "version": "2.0.0-next.4",
+    "version": "2.0.0-next.5",
     "description": "ESLint plugin for finding RegExp mistakes and RegExp style guide violations.",
     "engines": {
         "node": "^18 || >=20"


Test patch:
diff --git a/tests/lib/rules/no-misleading-unicode-character.ts b/tests/lib/rules/no-misleading-unicode-character.ts
index 6c871af..0fe3ada 100644
--- a/tests/lib/rules/no-misleading-unicode-character.ts
+++ b/tests/lib/rules/no-misleading-unicode-character.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-misleading-unicode-character"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -46,6 +46,13 @@ tester.run("no-misleading-unicode-character", rule as any, {
 
         // Ignore escaped symbols because it's obvious they aren't together
         `/[\\uD83D\\uDC4D]/`,
+
+        // ES2024
+        "var r = /[👍]/v",
+        String.raw`var r = /^[\q{👶🏻}]$/v`,
+        String.raw`var r = /[🇯\q{abc}🇵]/v`,
+        "var r = /[🇯[A]🇵]/v",
+        "var r = /[🇯[A--B]🇵]/v",
     ],
     invalid: [
         {
@@ -262,5 +269,58 @@ tester.run("no-misleading-unicode-character", rule as any, {
             options: [{ fixable: true }],
             errors: [{ messageId: "characterClass" }],
         },
+
+        // ES2024
+        {
+            code: String.raw`/[[👶🏻]]/v`,
+            output: String.raw`/[[\q{👶🏻}]]/v`,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "characterClass" }],
+        },
+        {
+            code: String.raw`/[👶🏻[👨‍👩‍👦]]/v`,
+            output: String.raw`/[\q{👶🏻}[👨‍👩‍👦]]/v`,
+            options: [{ fixable: true }],
+            errors: [
+                { messageId: "characterClass", column: 3 },
+                { messageId: "characterClass", column: 8 },
+            ],
+        },
+        {
+            code: String.raw`/[👶🏻👨‍👩‍👦]/v`,
+            output: String.raw`/[\q{👶🏻|👨‍👩‍👦}]/v`,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "characterClass" }],
+        },
+        {
+            code: String.raw`/[👶🏻&👨‍👩‍👦]/v`,
+            output: String.raw`/[\q{👶🏻|👨‍👩‍👦}&]/v`,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "characterClass" }],
+        },
+        {
+            code: String.raw`/[^👶🏻&👨‍👩‍👦]/v`,
+            output: null,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "characterClass" }],
+        },
+        {
+            code: String.raw`/[^👨‍👩‍👦]/v`,
+            output: null,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "characterClass" }],
+        },
+        {
+            code: String.raw`new RegExp("[👨‍👩‍👦]", "v")`,
+            output: String.raw`new RegExp("[\\q{👨‍👩‍👦}]", "v")`,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "characterClass" }],
+        },
+        {
+            code: `/👨‍👩‍👦+/v`,
+            output: `/(?:👨‍👩‍👦)+/v`,
+            options: [{ fixable: true }],
+            errors: [{ messageId: "quantifierMulti" }],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-useless-escape.ts b/tests/lib/rules/no-useless-escape.ts
index 8328c1b..f369682 100644
--- a/tests/lib/rules/no-useless-escape.ts
+++ b/tests/lib/rules/no-useless-escape.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-useless-escape"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -65,6 +65,60 @@ tester.run("no-useless-escape", rule as any, {
         //
         String.raw`/()\1\8/; /()\1\9/`,
         "/[\\^-`]/",
+
+        // ES2024
+        String.raw`/[\q{abc}]/v`,
+        String.raw`/[\(]/v`,
+        String.raw`/[\)]/v`,
+        String.raw`/[\{]/v`,
+        String.raw`/[\]]/v`,
+        String.raw`/[\}]/v`,
+        String.raw`/[\/]/v`,
+        String.raw`/[\-]/v`,
+        String.raw`/[\|]/v`,
+        String.raw`/[\$$]/v`,
+        String.raw`/[\&&]/v`,
+        String.raw`/[\!!]/v`,
+        String.raw`/[\##]/v`,
+        String.raw`/[\%%]/v`,
+        String.raw`/[\**]/v`,
+        String.raw`/[\++]/v`,
+        String.raw`/[\,,]/v`,
+        String.raw`/[\..]/v`,
+        String.raw`/[\::]/v`,
+        String.raw`/[\;;]/v`,
+        String.raw`/[\<<]/v`,
+        String.raw`/[\==]/v`,
+        String.raw`/[\>>]/v`,
+        String.raw`/[\??]/v`,
+        String.raw`/[\@@]/v`,
+        "/[\\``]/v",
+        String.raw`/[\~~]/v`,
+        String.raw`/[^\^^]/v`,
+        String.raw`/[_\^^]/v`,
+        String.raw`/[$\$]/v`,
+        String.raw`/[&\&]/v`,
+        String.raw`/[!\!]/v`,
+        String.raw`/[#\#]/v`,
+        String.raw`/[%\%]/v`,
+        String.raw`/[*\*]/v`,
+        String.raw`/[+\+]/v`,
+        String.raw`/[,\,]/v`,
+        String.raw`/[.\.]/v`,
+        String.raw`/[:\:]/v`,
+        String.raw`/[;\;]/v`,
+        String.raw`/[<\<]/v`,
+        String.raw`/[=\=]/v`,
+        String.raw`/[>\>]/v`,
+        String.raw`/[?\?]/v`,
+        String.raw`/[@\@]/v`,
+        "/[`\\`]/v",
+        String.raw`/[~\~]/v`,
+        String.raw`/[^^\^]/v`,
+        String.raw`/[_^\^]/v`,
+        String.raw`/[\&&&\&]/v`,
+        String.raw`/[[\-]\-]/v`,
+        String.raw`/[\^]/v`,
     ],
     invalid: [
         {
@@ -128,5 +182,373 @@ tester.run("no-useless-escape", rule as any, {
                 "Unnecessary escape character: \\P.",
             ],
         },
+        {
+            code: String.raw`/[\q{abc}]/;`, // Missing v flag
+            output: null,
+            errors: ["Unnecessary escape character: \\q."],
+        },
+
+        // ES2024
+        {
+            code: String.raw`/[\$]/v`,
+            output: String.raw`/[$]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    endColumn: 4,
+                    message: "Unnecessary escape character: \\$.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\&\&]/v`,
+            output: String.raw`/[&\&]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\&.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\!\!]/v`,
+            output: String.raw`/[!\!]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\!.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\#\#]/v`,
+            output: String.raw`/[#\#]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\#.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\%\%]/v`,
+            output: String.raw`/[%\%]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\%.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\*\*]/v`,
+            output: String.raw`/[*\*]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\*.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\+\+]/v`,
+            output: String.raw`/[+\+]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\+.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\,\,]/v`,
+            output: String.raw`/[,\,]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\,.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\.\.]/v`,
+            output: String.raw`/[.\.]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\:\:]/v`,
+            output: String.raw`/[:\:]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\:.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\;\;]/v`,
+            output: String.raw`/[;\;]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\;.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\<\<]/v`,
+            output: String.raw`/[<\<]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\<.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\=\=]/v`,
+            output: String.raw`/[=\=]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\=.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\>\>]/v`,
+            output: String.raw`/[>\>]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\>.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\?\?]/v`,
+            output: String.raw`/[?\?]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\?.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\@\@]/v`,
+            output: String.raw`/[@\@]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\@.",
+                },
+            ],
+        },
+        {
+            code: "/[\\`\\`]/v",
+            output: "/[`\\`]/v",
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\`.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\~\~]/v`,
+            output: String.raw`/[~\~]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\~.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[^\^\^]/v`,
+            output: String.raw`/[^^\^]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 4,
+                    message: "Unnecessary escape character: \\^.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[_\^\^]/v`,
+            output: String.raw`/[_^\^]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 4,
+                    message: "Unnecessary escape character: \\^.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[^\^]/v`,
+            output: String.raw`/[^^]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 4,
+                    message: "Unnecessary escape character: \\^.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\&\&&\&]/v`,
+            output: String.raw`/[&\&&\&]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\&.",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\p{ASCII}--\.]/v`,
+            output: String.raw`/[\p{ASCII}--.]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 14,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\p{ASCII}&&\.]/v`,
+            output: String.raw`/[\p{ASCII}&&.]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 14,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\.--[.&]]/v`,
+            output: String.raw`/[.--[.&]]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\.&&[.&]]/v`,
+            output: String.raw`/[.&&[.&]]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\.--\.--\.]/v`,
+            output: String.raw`/[.--.--.]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\..",
+                },
+                {
+                    line: 1,
+                    column: 7,
+                    message: "Unnecessary escape character: \\..",
+                },
+                {
+                    line: 1,
+                    column: 11,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\.&&\.&&\.]/v`,
+            output: String.raw`/[.&&.&&.]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 3,
+                    message: "Unnecessary escape character: \\..",
+                },
+                {
+                    line: 1,
+                    column: 7,
+                    message: "Unnecessary escape character: \\..",
+                },
+                {
+                    line: 1,
+                    column: 11,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[[\.&]--[\.&]]/v`,
+            output: String.raw`/[[.&]--[.&]]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 4,
+                    message: "Unnecessary escape character: \\..",
+                },
+                {
+                    line: 1,
+                    column: 11,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
+        {
+            code: String.raw`/[[\.&]&&[\.&]]/v`,
+            output: String.raw`/[[.&]&&[.&]]/v`,
+            errors: [
+                {
+                    line: 1,
+                    column: 4,
+                    message: "Unnecessary escape character: \\..",
+                },
+                {
+                    line: 1,
+                    column: 11,
+                    message: "Unnecessary escape character: \\..",
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/prefer-d.ts b/tests/lib/rules/prefer-d.ts
index bdae490..e01e1e2 100644
--- a/tests/lib/rules/prefer-d.ts
+++ b/tests/lib/rules/prefer-d.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/prefer-d"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -28,6 +28,14 @@ tester.run("prefer-d", rule as any, {
             code: String.raw`/[\da-z]/`,
             options: [{ insideCharacterClass: "d" }],
         },
+        String.raw`/\d/v`,
+        {
+            code: String.raw`/[\d--0]/v`,
+            options: [{ insideCharacterClass: "range" }],
+        },
+        String.raw`/[\q{0|1|2|3|4|5|6|7|8}]/v`,
+        String.raw`/[\q{0|1|2|3|4|5|6|7|8|9|a}]/v`,
+        String.raw`/[\q{0|1|2|3|4|5|6|7|8|9|abc}]/v`,
     ],
     invalid: [
         {
@@ -99,5 +107,79 @@ tester.run("prefer-d", rule as any, {
             options: [{ insideCharacterClass: "range" }],
             errors: ["Unexpected character set '\\d'. Use '0-9' instead."],
         },
+        {
+            code: "/[0-9]/v",
+            output: String.raw`/\d/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected character class '[0-9]'. Use '\\d' instead.",
+                    column: 2,
+                    endColumn: 7,
+                },
+            ],
+        },
+        {
+            code: "/[[0-9]--[0-7]]/v",
+            output: String.raw`/[\d--[0-7]]/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected character class '[0-9]'. Use '\\d' instead.",
+                    column: 3,
+                    endColumn: 8,
+                },
+            ],
+        },
+        {
+            code: "/[[0-:]--:]/v",
+            output: String.raw`/\d/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected character class '[[0-:]--:]'. Use '\\d' instead.",
+                    column: 2,
+                    endColumn: 12,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[[\da-z]--0]/v`,
+            output: String.raw`/[[0-9a-z]--0]/v`,
+            options: [{ insideCharacterClass: "range" }],
+            errors: [
+                {
+                    message:
+                        "Unexpected character set '\\d'. Use '0-9' instead.",
+                    column: 4,
+                    endColumn: 6,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[[0-9a-z]--0]/v`,
+            output: String.raw`/[[\da-z]--0]/v`,
+            options: [{ insideCharacterClass: "d" }],
+            errors: [
+                {
+                    message:
+                        "Unexpected character class range '0-9'. Use '\\d' instead.",
+                    column: 4,
+                    endColumn: 7,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{0|1|2|3|4|5|6|7|8|9}]/v`,
+            output: String.raw`/\d/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected character class '[\\q{0|1|2|3|4|5|6|7|8|9}]'. Use '\\d' instead.",
+                    column: 2,
+                    endColumn: 27,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/prefer-unicode-codepoint-escapes.ts b/tests/lib/rules/prefer-unicode-codepoint-escapes.ts
index 9a87ad9..b3e68aa 100644
--- a/tests/lib/rules/prefer-unicode-codepoint-escapes.ts
+++ b/tests/lib/rules/prefer-unicode-codepoint-escapes.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/prefer-unicode-codepoint-escapes"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -15,6 +15,8 @@ tester.run("prefer-unicode-codepoint-escapes", rule as any, {
         String.raw`/[\ud83d\ude00]/`,
         String.raw`/\u{1f600}/u`,
         String.raw`/😀/u`,
+        String.raw`/\u{1f600}/v`,
+        String.raw`/😀/v`,
     ],
     invalid: [
         {
@@ -75,5 +77,19 @@ tester.run("prefer-unicode-codepoint-escapes", rule as any, {
                 "Use Unicode codepoint escapes instead of Unicode escapes using surrogate pairs.",
             ],
         },
+        {
+            code: String.raw`/\ud83d\ude00/v`,
+            output: String.raw`/\u{1f600}/v`,
+            errors: [
+                {
+                    message:
+                        "Use Unicode codepoint escapes instead of Unicode escapes using surrogate pairs.",
+                    line: 1,
+                    column: 2,
+                    endLine: 1,
+                    endColumn: 14,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/prefer-w.ts b/tests/lib/rules/prefer-w.ts
index d7d03cf..873ddd5 100644
--- a/tests/lib/rules/prefer-w.ts
+++ b/tests/lib/rules/prefer-w.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/prefer-w"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("prefer-w", rule as any, {
-    valid: ["/\\w/"],
+    valid: ["/\\w/", "/[\\Da-zA-Z_#]/", "/\\w/v", "/[\\Da-zA-Z_#]/v"],
     invalid: [
         {
             code: "/[0-9a-zA-Z_]/",
@@ -35,6 +35,20 @@ tester.run("prefer-w", rule as any, {
                 },
             ],
         },
+        {
+            code: "/[\\da-zA-Z_#]/",
+            output: "/[\\w#]/",
+            errors: [
+                "Unexpected character class ranges '[\\da-zA-Z_]'. Use '\\w' instead.",
+            ],
+        },
+        {
+            code: "/[0-9a-z_[\\s&&\\p{ASCII}]]/iv",
+            output: "/[\\w[\\s&&\\p{ASCII}]]/iv",
+            errors: [
+                "Unexpected character class ranges '[0-9a-z_]'. Use '\\w' instead.",
+            ],
+        },
         {
             code: "/[0-9a-z_]/i",
             output: "/\\w/i",
diff --git a/tests/lib/rules/require-unicode-regexp.ts b/tests/lib/rules/require-unicode-regexp.ts
index aee0415..a3025e9 100644
--- a/tests/lib/rules/require-unicode-regexp.ts
+++ b/tests/lib/rules/require-unicode-regexp.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/require-unicode-regexp"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -28,6 +28,8 @@ tester.run("require-unicode-regexp", rule as any, {
         String.raw`const flags = 'u'; new globalThis.RegExp('', flags)`,
         String.raw`const flags = 'g'; new globalThis.RegExp('', flags + 'u')`,
         String.raw`const flags = 'gimu'; new globalThis.RegExp('foo', flags[3])`,
+        String.raw`/foo/v`,
+        String.raw`new RegExp('foo', 'v')`,
     ],
     invalid: [
         {
diff --git a/tests/lib/rules/unicode-escape.ts b/tests/lib/rules/unicode-escape.ts
index c04b81c..2e8c76d 100644
--- a/tests/lib/rules/unicode-escape.ts
+++ b/tests/lib/rules/unicode-escape.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/unicode-escape"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -19,6 +19,14 @@ tester.run("unicode-escape", rule as any, {
             code: String.raw`/a \x0a \cM \0 \u0100 \u00ff \ud83d\ude00 \u{1f600}/u`,
             options: ["unicodeEscape"],
         },
+        {
+            code: String.raw`/a \x0a \cM \0 \u{ff} \u{100} \ud83d\ude00 \u{1f600}/v`,
+            options: ["unicodeCodePointEscape"],
+        },
+        {
+            code: String.raw`/a \x0a \cM \0 \u0100 \u00ff \ud83d\ude00 \u{1f600}/v`,
+            options: ["unicodeEscape"],
+        },
 
         // no u flag
         {
@@ -77,5 +85,39 @@ tester.run("unicode-escape", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/a \x0a \cM \0 \u0100 \u00ff \ud83d\ude00 \u{1f600}/v`,
+            output: String.raw`/a \x0a \cM \0 \u{100} \u{ff} \ud83d\ude00 \u{1f600}/v`,
+            options: ["unicodeCodePointEscape"],
+            errors: [
+                {
+                    message:
+                        "Expected unicode code point escape ('\\u{100}'), but unicode escape ('\\u0100') is used.",
+                    column: 16,
+                },
+                {
+                    message:
+                        "Expected unicode code point escape ('\\u{ff}'), but unicode escape ('\\u00ff') is used.",
+                    column: 23,
+                },
+            ],
+        },
+        {
+            code: String.raw`/a \x0a \cM \0 \u{ff} \u{100} \ud83d\ude00 \u{1f600}/v`,
+            output: String.raw`/a \x0a \cM \0 \u00ff \u0100 \ud83d\ude00 \u{1f600}/v`,
+            options: ["unicodeEscape"],
+            errors: [
+                {
+                    message:
+                        "Expected unicode escape ('\\u00ff'), but unicode code point escape ('\\u{ff}') is used.",
+                    column: 16,
+                },
+                {
+                    message:
+                        "Expected unicode escape ('\\u0100'), but unicode code point escape ('\\u{100}') is used.",
+                    column: 23,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/utils/regexp-ast.ts b/tests/lib/utils/regexp-ast.ts
index 6d9d80e..3214b36 100644
--- a/tests/lib/utils/regexp-ast.ts
+++ b/tests/lib/utils/regexp-ast.ts
@@ -202,6 +202,11 @@ const TESTCASES_FOR_IS_EQUAL_NODES: TestCase[] = [
         b: /ya?ml/u,
         result: true,
     },
+    {
+        a: String.raw`/[\q{foo|bar}]/v`,
+        b: String.raw`/[\q{bar|foo}]/v`,
+        result: true,
+    },
 ]
 describe("regexp-ast isEqualNodes", () => {
     for (const testCase of TESTCASES_FOR_IS_EQUAL_NODES) {