Repository: ota-meshi/eslint-plugin-regexp
Pull request #588 (2023-09-09 14:33:18)

Add support for v flag to `regexp/sort-character-class-elements` rule
related to https://github.com/ota-meshi/eslint-plugin-regexp/issues/545

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

Latest commit: 6c4bd4f84d802109b5e5f8735c9ef69f6069a4b8

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/sort-character-class-elements?filename=.changeset/green-peaches-explode.md&value=---%0A%22eslint-plugin-regexp%22%3A%20patch%0A---%0A%0AAdd%20support%20for%20v%20flag%20to%20%60regexp%2Fsort-character-class-elements%60%20rule%0A)



Patch:
diff --git a/.changeset/beige-fireants-dress.md b/.changeset/beige-fireants-dress.md
new file mode 100644
index 0000000..41d08ab
--- /dev/null
+++ b/.changeset/beige-fireants-dress.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for `v` flag to `regexp/no-dupe-disjunctions`
diff --git a/.changeset/green-peaches-explode.md b/.changeset/green-peaches-explode.md
new file mode 100644
index 0000000..095910f
--- /dev/null
+++ b/.changeset/green-peaches-explode.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for v flag to `regexp/sort-character-class-elements` rule
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 0822ece..f9f6b7e 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -5,12 +5,15 @@
     "eslint-plugin-regexp": "1.15.0"
   },
   "changesets": [
+    "beige-fireants-dress",
     "brown-dragons-dance",
     "calm-onions-yell",
     "clever-jobs-clap",
     "curvy-shoes-kiss",
     "cyan-waves-notice",
     "early-guests-hug",
+    "friendly-walls-reply",
+    "gold-baboons-clap",
     "heavy-rings-reply",
     "hungry-spoons-mix",
     "khaki-colts-tell",
@@ -18,13 +21,16 @@
     "loud-flowers-search",
     "lovely-brooms-explode",
     "neat-rats-sneeze",
+    "rich-ways-exercise",
     "short-dingos-dance",
     "six-squids-look",
     "small-doors-lie",
     "smart-chefs-poke",
+    "sour-feet-explain",
     "tasty-penguins-cheat",
     "thin-ears-relax",
     "warm-ladybugs-wonder",
-    "wicked-rules-rhyme"
+    "wicked-rules-rhyme",
+    "young-hairs-bow"
   ]
 }
diff --git a/.changeset/young-hairs-bow.md b/.changeset/young-hairs-bow.md
new file mode 100644
index 0000000..4f6d601
--- /dev/null
+++ b/.changeset/young-hairs-bow.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for `v` flag to `regexp/prefer-predefined-assertion`
diff --git a/.github/workflows/GHPages.yml b/.github/workflows/GHPages.yml
index c61890d..2b3ee3a 100644
--- a/.github/workflows/GHPages.yml
+++ b/.github/workflows/GHPages.yml
@@ -24,7 +24,7 @@ jobs:
         runs-on: ubuntu-latest
         steps:
             - name: Checkout
-              uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+              uses: actions/checkout@v4
             - uses: actions/setup-node@v3
               with:
                   node-version: 18
diff --git a/.github/workflows/NodeCI.yml b/.github/workflows/NodeCI.yml
index abacad0..5d434a7 100644
--- a/.github/workflows/NodeCI.yml
+++ b/.github/workflows/NodeCI.yml
@@ -10,7 +10,7 @@ jobs:
     lint:
         runs-on: ubuntu-latest
         steps:
-            - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+            - uses: actions/checkout@v4
             - uses: actions/setup-node@v3
               with:
                   node-version: 18
@@ -26,7 +26,7 @@ jobs:
             matrix:
                 node-version: [18.x, 20.x]
         steps:
-            - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+            - uses: actions/checkout@v4
             - name: Use Node.js ${{ matrix.node-version }}
               uses: actions/setup-node@v3
               with:
@@ -38,7 +38,7 @@ jobs:
     test-and-coverage:
         runs-on: ubuntu-latest
         steps:
-            - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+            - uses: actions/checkout@v4
             - uses: actions/setup-node@v3
             - name: Install Packages
               run: npm ci
@@ -51,7 +51,7 @@ jobs:
     update:
         runs-on: ubuntu-latest
         steps:
-            - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+            - uses: actions/checkout@v4
             - uses: actions/setup-node@v3
               with:
                   node-version: 18
diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml
index 8cfa6ac..879b388 100644
--- a/.github/workflows/Release.yml
+++ b/.github/workflows/Release.yml
@@ -16,7 +16,7 @@ jobs:
         runs-on: ubuntu-latest
         steps:
             - name: Checkout Repo
-              uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+              uses: actions/checkout@v4
               with:
                   # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
                   fetch-depth: 0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80587e7..62de131 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
 # eslint-plugin-regexp
 
+## 2.0.0-next.7
+
+### Minor Changes
+
+-   Add support for `v` flag to `regexp/no-dupe-disjunctions` ([#612](https://github.com/ota-meshi/eslint-plugin-regexp/pull/612))
+
+-   Add support for v flag to `regexp/no-dupe-characters-character-class` rule ([#608](https://github.com/ota-meshi/eslint-plugin-regexp/pull/608))
+
+-   Add support for v flag to `regexp/no-useless-character-class` rule ([#593](https://github.com/ota-meshi/eslint-plugin-regexp/pull/593))
+
+-   Improve `regexp/sort-alternatives` rule to add support for string alternatives and v flag ([#587](https://github.com/ota-meshi/eslint-plugin-regexp/pull/587))
+
+-   Add `regexp/require-unicode-sets-regexp` rule ([#598](https://github.com/ota-meshi/eslint-plugin-regexp/pull/598))
+
+-   Add support for `v` flag to `regexp/prefer-predefined-assertion` ([#611](https://github.com/ota-meshi/eslint-plugin-regexp/pull/611))
+
 ## 2.0.0-next.6
 
 ### Minor Changes
diff --git a/docs/.vitepress/theme/components/state/deserialize.js b/docs/.vitepress/theme/components/state/deserialize.js
index 8fb06a6..a5062f2 100644
--- a/docs/.vitepress/theme/components/state/deserialize.js
+++ b/docs/.vitepress/theme/components/state/deserialize.js
@@ -16,13 +16,32 @@ export function deserializeState(serializedString) {
     }
 
     try {
-        // For backward compatibility, it can address non-compressed data.
-        const compressed = !serializedString.startsWith("eyJj")
-        const decodedText = window.atob(serializedString)
-        const jsonText = compressed
-            ? pako.inflate(decodedText, { to: "string" })
-            : decodedText
-        const json = JSON.parse(jsonText)
+        // For backward compatibility
+        const newCompressed = serializedString.startsWith("eJ")
+        const json = (
+            newCompressed
+                ? () => {
+                      const compressedString = window.atob(serializedString)
+                      const uint8Arr = pako.inflate(
+                          Uint8Array.from(compressedString, (c) =>
+                              c.charCodeAt(0),
+                          ),
+                      )
+                      const jsonText = new TextDecoder().decode(uint8Arr)
+                      return JSON.parse(jsonText)
+                  }
+                : () => {
+                      const decodedText = window.atob(serializedString)
+                      try {
+                          const jsonText = pako.inflate(decodedText, {
+                              to: "string",
+                          })
+                          return JSON.parse(jsonText)
+                      } catch {
+                          return JSON.parse(decodedText)
+                      }
+                  }
+        )()
 
         if (typeof json === "object" && json != null) {
             if (typeof json.code === "string") {
diff --git a/docs/.vitepress/theme/components/state/serialize.js b/docs/.vitepress/theme/components/state/serialize.js
index 5ad5d50..fd4045c 100644
--- a/docs/.vitepress/theme/components/state/serialize.js
+++ b/docs/.vitepress/theme/components/state/serialize.js
@@ -25,7 +25,8 @@ export function serializeState(state) {
         rules: state.rules ? getEnabledRules(state.rules) : undefined,
     }
     const jsonString = JSON.stringify(saveData)
-    const compressedString = pako.deflate(jsonString, { to: "string" })
+    const uint8Arr = new TextEncoder().encode(jsonString)
+    const compressedString = String.fromCharCode(...pako.deflate(uint8Arr))
     const base64 =
         (typeof window !== "undefined" && window.btoa(compressedString)) ||
         compressedString
diff --git a/docs/rules/require-unicode-sets-regexp.md b/docs/rules/require-unicode-sets-regexp.md
index c3306d8..d074dd6 100644
--- a/docs/rules/require-unicode-sets-regexp.md
+++ b/docs/rules/require-unicode-sets-regexp.md
@@ -3,6 +3,7 @@ pageClass: "rule-details"
 sidebarDepth: 0
 title: "regexp/require-unicode-sets-regexp"
 description: "enforce the use of the `v` flag"
+since: "v2.0.0-next.7"
 ---
 # regexp/require-unicode-sets-regexp
 
@@ -60,7 +61,7 @@ Nothing.
 
 ## :rocket: Version
 
-:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+This rule was introduced in eslint-plugin-regexp v2.0.0-next.7
 
 ## :mag: Implementation
 
diff --git a/docs/rules/sort-character-class-elements.md b/docs/rules/sort-character-class-elements.md
index 8942ef8..fa487ed 100644
--- a/docs/rules/sort-character-class-elements.md
+++ b/docs/rules/sort-character-class-elements.md
@@ -44,12 +44,14 @@ var foo = /[b-fa]/
       "\\d", // \d or \D
       "\\p", // \p{...} or \P{...}
       "*", // Others (A character or range of characters or an element you did not specify.)
+      "\\q", // \q{...}
+      "[]", // Nesting character class, or character class expression
     ]
   }]
 }
 ```
 
-- `"order"` ... An array of your preferred order. The default is `["\\s", "\\w", "\\d", "\\p", "*",]`.
+- `"order"` ... An array of your preferred order. The default is `["\\s", "\\w", "\\d", "\\p", "*", "\\q", "[]"]`.
 
 ## :rocket: Version
 
diff --git a/lib/rules/no-dupe-disjunctions.ts b/lib/rules/no-dupe-disjunctions.ts
index 09b7f2c..fe41553 100644
--- a/lib/rules/no-dupe-disjunctions.ts
+++ b/lib/rules/no-dupe-disjunctions.ts
@@ -2,6 +2,7 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
 import type {
     Alternative,
     CapturingGroup,
+    CharacterClassElement,
     Group,
     LookaroundAssertion,
     Node,
@@ -15,6 +16,7 @@ import {
     fixRemoveCharacterClassElement,
     fixRemoveAlternative,
     assertValidFlags,
+    fixRemoveStringAlternative,
 } from "../utils"
 import { getParser, isCoveredNode, isEqualNodes } from "../utils/regexp-ast"
 import type { Expression, FiniteAutomaton, NoParent, ReadonlyNFA } from "refa"
@@ -221,21 +223,46 @@ function* iterateNestedAlternatives(
 
         if (e.type === "CharacterClass" && !e.negate) {
             const nested: NestedAlternative[] = []
-            for (const charElement of e.elements) {
-                if (charElement.type === "CharacterClassRange") {
-                    const min = charElement.min
-                    const max = charElement.max
-                    if (min.value === max.value) {
+
+            // eslint-disable-next-line func-style -- x
+            const addToNested = (charElement: CharacterClassElement) => {
+                switch (charElement.type) {
+                    case "CharacterClassRange": {
+                        const min = charElement.min
+                        const max = charElement.max
+                        if (min.value === max.value) {
+                            nested.push(charElement)
+                        } else if (min.value + 1 === max.value) {
+                            nested.push(min, max)
+                        } else {
+                            nested.push(charElement, min, max)
+                        }
+                        break
+                    }
+                    case "ClassStringDisjunction": {
+                        nested.push(...charElement.alternatives)
+                        break
+                    }
+                    case "CharacterClass": {
+                        if (!charElement.negate) {
+                            charElement.elements.forEach(addToNested)
+                        } else {
+                            nested.push(charElement)
+                        }
+                        break
+                    }
+                    case "Character":
+                    case "CharacterSet":
+                    case "ExpressionCharacterClass": {
                         nested.push(charElement)
-                    } else if (min.value + 1 === max.value) {
-                        nested.push(min, max)
-                    } else {
-                        nested.push(charElement, min, max)
+                        break
                     }
-                } else {
-                    nested.push(charElement)
+                    default:
+                        throw assertNever(charElement)
                 }
             }
+            e.elements.forEach(addToNested)
+
             if (nested.length > 1) yield* nested
         }
     }
@@ -825,7 +852,7 @@ function deduplicateResults(
 }
 
 function mentionNested(nested: NestedAlternative): string {
-    if (nested.type === "Alternative") {
+    if (nested.type === "Alternative" || nested.type === "StringAlternative") {
         return mention(nested)
     }
     return mentionChar(nested)
@@ -842,9 +869,15 @@ function fixRemoveNestedAlternative(
         case "Alternative":
             return fixRemoveAlternative(context, alternative)
 
+        case "StringAlternative":
+            return fixRemoveStringAlternative(context, alternative)
+
         case "Character":
         case "CharacterClassRange":
-        case "CharacterSet": {
+        case "CharacterSet":
+        case "CharacterClass":
+        case "ExpressionCharacterClass":
+        case "ClassStringDisjunction": {
             if (alternative.parent.type !== "CharacterClass") {
                 // This isn't supposed to happen. We can't just remove the only
                 // alternative of its parent
@@ -855,8 +888,6 @@ function fixRemoveNestedAlternative(
         }
 
         default:
-            // FIXME: TS Error
-            // @ts-expect-error -- FIXME
             throw assertNever(alternative)
     }
 }
diff --git a/lib/rules/prefer-predefined-assertion.ts b/lib/rules/prefer-predefined-assertion.ts
index 2c59de1..551b23f 100644
--- a/lib/rules/prefer-predefined-assertion.ts
+++ b/lib/rules/prefer-predefined-assertion.ts
@@ -2,6 +2,7 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
 import type {
     CharacterClass,
     CharacterSet,
+    ExpressionCharacterClass,
     LookaroundAssertion,
 } from "@eslint-community/regexpp/ast"
 import type { RegExpContext } from "../utils"
@@ -11,7 +12,7 @@ import {
     getFirstCharAfter,
     getMatchingDirectionFromAssertionKind,
     invertMatchingDirection,
-    toCharSet,
+    toUnicodeSet,
 } from "regexp-ast-analysis"
 
 /**
@@ -20,14 +21,15 @@ import {
  */
 function getCharacters(
     lookaround: LookaroundAssertion,
-): CharacterSet | CharacterClass | null {
+): CharacterSet | CharacterClass | ExpressionCharacterClass | null {
     if (lookaround.alternatives.length === 1) {
         const alt = lookaround.alternatives[0]
         if (alt.elements.length === 1) {
             const first = alt.elements[0]
             if (
                 first.type === "CharacterSet" ||
-                first.type === "CharacterClass"
+                first.type === "CharacterClass" ||
+                first.type === "ExpressionCharacterClass"
             ) {
                 return first
             }
@@ -200,9 +202,12 @@ export default createRule("prefer-predefined-assertion", {
                         }
                     }
 
-                    // FIXME: TS Error
-                    // @ts-expect-error -- FIXME
-                    const charSet = toCharSet(chars, flags)
+                    const set = toUnicodeSet(chars, flags)
+                    if (!set.accept.isEmpty) {
+                        // the set contains strings, so it can't be replaced with a predefined assertion
+                        return
+                    }
+                    const charSet = set.chars
                     if (charSet.isAll) {
                         replaceEdgeAssertion(aNode, false)
                     } else if (charSet.equals(word)) {
diff --git a/lib/rules/sort-character-class-elements.ts b/lib/rules/sort-character-class-elements.ts
index ae2b515..d29c68a 100644
--- a/lib/rules/sort-character-class-elements.ts
+++ b/lib/rules/sort-character-class-elements.ts
@@ -2,24 +2,34 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
 import type {
     CharacterClass,
     CharacterClassElement,
+    ClassRangesCharacterClassElement,
     UnicodePropertyCharacterSet,
+    UnicodeSetsCharacterClassElement,
 } from "@eslint-community/regexpp/ast"
 import type { RegExpContext } from "../utils"
-import {
-    CP_DIGIT_ZERO,
-    CP_SPACE,
-    createRule,
-    defineRegexpVisitor,
-} from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
 import { mention } from "../utils/mention"
+import type { ReadonlyFlags } from "regexp-ast-analysis"
+import { toUnicodeSet } from "regexp-ast-analysis"
+import type { ReadonlyWord } from "refa"
+import { getLexicographicallySmallest } from "../utils/lexicographically-smallest"
 
-type CharacterClassElementKind = "\\w" | "\\d" | "\\s" | "\\p" | "*"
+type CharacterClassElementKind =
+    | "\\w"
+    | "\\d"
+    | "\\s"
+    | "\\p"
+    | "*"
+    | "\\q"
+    | "[]"
 const DEFAULT_ORDER: CharacterClassElementKind[] = [
     "\\s",
     "\\w",
     "\\d",
     "\\p",
     "*",
+    "\\q",
+    "[]",
 ]
 
 /**
@@ -37,9 +47,46 @@ function getCharacterClassElementKind(
             ? "\\s"
             : "\\p"
     }
+    if (node.type === "ClassStringDisjunction") {
+        return "\\q"
+    }
+    if (
+        node.type === "CharacterClass" ||
+        node.type === "ExpressionCharacterClass"
+    ) {
+        return "[]"
+    }
     return "*"
 }
 
+/**
+ * Return the lexicographically smallest string accepted by the given element.
+ * If the class set is negate, the original value is used for calculation.
+ */
+function getLexicographicallySmallestFromElement(
+    node: CharacterClassElement,
+    flags: ReadonlyFlags,
+): ReadonlyWord {
+    const us =
+        node.type === "CharacterSet" && node.negate
+            ? toUnicodeSet({ ...node, negate: false }, flags)
+            : toUnicodeSet(node, flags)
+    return getLexicographicallySmallest(us) || []
+}
+
+/**
+ * Compare two strings of char sets by byte order.
+ */
+function compareWords(a: ReadonlyWord, b: ReadonlyWord): number {
+    const l = Math.min(a.length, b.length)
+    for (let i = 0; i < l; i++) {
+        const aI = a[i]
+        const bI = b[i]
+        if (aI !== bI) return aI - bI
+    }
+    return a.length - b.length
+}
+
 export default createRule("sort-character-class-elements", {
     meta: {
         docs: {
@@ -54,7 +101,17 @@ export default createRule("sort-character-class-elements", {
                 properties: {
                     order: {
                         type: "array",
-                        items: { enum: ["\\w", "\\d", "\\s", "\\p", "*"] },
+                        items: {
+                            enum: [
+                                "\\s",
+                                "\\w",
+                                "\\d",
+                                "\\p",
+                                "*",
+                                "\\q",
+                                "[]",
+                            ],
+                        },
                     },
                 },
                 additionalProperties: false,
@@ -73,6 +130,8 @@ export default createRule("sort-character-class-elements", {
             "\\d"?: number
             "\\s"?: number
             "\\p"?: number
+            "\\q"?: number
+            "[]"?: number
         } = { "*": Infinity }
 
         ;(
@@ -84,6 +143,7 @@ export default createRule("sort-character-class-elements", {
 
         function createVisitor({
             node,
+            flags,
             getRegexpLocation,
             patternSource,
         }: RegExpContext): RegExpVisitor.Handlers {
@@ -93,10 +153,10 @@ export default createRule("sort-character-class-elements", {
                     for (const next of ccNode.elements) {
                         if (prevList.length) {
                             const prev = prevList[0]
-                            if (!isValidOrder(prev, next)) {
+                            if (!isValidOrder(prev, next, flags)) {
                                 let moveTarget = prev
                                 for (const p of prevList) {
-                                    if (isValidOrder(p, next)) {
+                                    if (isValidOrder(p, next, flags)) {
                                         break
                                     } else {
                                         moveTarget = p
@@ -144,6 +204,7 @@ export default createRule("sort-character-class-elements", {
         function isValidOrder(
             prev: CharacterClassElement,
             next: CharacterClassElement,
+            flags: ReadonlyFlags,
         ) {
             const prevKind = getCharacterClassElementKind(prev)
             const nextKind = getCharacterClassElementKind(next)
@@ -154,41 +215,33 @@ export default createRule("sort-character-class-elements", {
             } else if (prevOrder > nextOrder) {
                 return false
             }
-            if (prev.type === "CharacterSet" && prev.kind === "property") {
-                if (next.type === "CharacterSet") {
-                    if (next.kind === "property") {
-                        return isValidOrderForUnicodePropertyCharacterSet(
-                            prev,
-                            next,
-                        )
-                    }
-                    // e.g. /[\p{ASCII}\d]/
-                    return false
-                }
-                // e.g. /[\p{ASCII}a]/
+
+            const prevOrderShortCircuit = DEFAULT_ORDER.indexOf(prevKind)
+            const nextOrderShortCircuit = DEFAULT_ORDER.indexOf(nextKind)
+            if (prevOrderShortCircuit < nextOrderShortCircuit) {
                 return true
-            } else if (
+            } else if (prevOrderShortCircuit > nextOrderShortCircuit) {
+                return false
+            }
+
+            if (
+                prev.type === "CharacterSet" &&
+                prev.kind === "property" &&
                 next.type === "CharacterSet" &&
                 next.kind === "property"
             ) {
-                if (prev.type === "CharacterSet") {
-                    // e.g. /[\d\p{ASCII}]/
-                    return true
-                }
-                // e.g. /[a\p{ASCII}]/
-                return false
+                return isValidOrderForUnicodePropertyCharacterSet(prev, next)
             }
-            if (prev.type === "CharacterSet" && next.type === "CharacterSet") {
-                if (prev.kind === "word" && next.kind === "digit") {
-                    return true
-                }
-                if (prev.kind === "digit" && next.kind === "word") {
-                    return false
-                }
-            }
-            const prevCP = getTargetCodePoint(prev)
-            const nextCP = getTargetCodePoint(next)
-            if (prevCP <= nextCP) {
+
+            const prevWord = getLexicographicallySmallestFromElement(
+                prev,
+                flags,
+            )
+            const nextWord = getLexicographicallySmallestFromElement(
+                next,
+                flags,
+            )
+            if (compareWords(prevWord, nextWord) <= 0) {
                 return true
             }
             return false
@@ -218,29 +271,6 @@ export default createRule("sort-character-class-elements", {
             return true
         }
 
-        /**
-         * Gets the target code point for a given element.
-         */
-        function getTargetCodePoint(
-            node: Exclude<CharacterClassElement, UnicodePropertyCharacterSet>,
-        ) {
-            if (node.type === "CharacterSet") {
-                if (node.kind === "digit" || node.kind === "word") {
-                    return CP_DIGIT_ZERO
-                }
-                if (node.kind === "space") {
-                    return CP_SPACE
-                }
-                return Infinity
-            }
-            if (node.type === "CharacterClassRange") {
-                return node.min.value
-            }
-            // FIXME: TS Error
-            // @ts-expect-error -- FIXME
-            return node.value
-        }
-
         return defineRegexpVisitor(context, {
             createVisitor,
         })
@@ -254,9 +284,11 @@ function escapeRaw(node: CharacterClassElement, target: CharacterClassElement) {
     let raw = node.raw
     if (raw.startsWith("-")) {
         const parent = target.parent as CharacterClass
-        // FIXME: TS Error
-        // @ts-expect-error -- FIXME
-        const prev = parent.elements[parent.elements.indexOf(target) - 1]
+        const elements: (
+            | UnicodeSetsCharacterClassElement
+            | ClassRangesCharacterClassElement
+        )[] = parent.elements
+        const prev = elements[elements.indexOf(target) - 1]
         if (
             prev &&
             (prev.type === "Character" || prev.type === "CharacterSet")
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index a550d6c..cb3eda1 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -9,6 +9,7 @@ import type {
     Node,
     Pattern,
     Quantifier,
+    StringAlternative,
 } from "@eslint-community/regexpp/ast"
 import { RegExpParser, visitRegExpAST } from "@eslint-community/regexpp"
 import {
@@ -1206,14 +1207,13 @@ export function fixRemoveCharacterClassElement(
             return null
         }
 
+        const elements: CharacterClassElement[] = cc.elements
+        const elementIndex = elements.indexOf(element)
+
         const elementBefore: CharacterClassElement | undefined =
-            // FIXME: TS Error
-            // @ts-expect-error -- FIXME
-            cc.elements[cc.elements.indexOf(element) - 1]
+            cc.elements[elementIndex - 1]
         const elementAfter: CharacterClassElement | undefined =
-            // FIXME: TS Error
-            // @ts-expect-error -- FIXME
-            cc.elements[cc.elements.indexOf(element) + 1]
+            cc.elements[elementIndex + 1]
 
         if (
             elementBefore &&
@@ -1275,6 +1275,32 @@ export function fixRemoveAlternative(
     })
 }
 
+export function fixRemoveStringAlternative(
+    context: RegExpContext,
+    alternative: StringAlternative,
+): (fixer: Rule.RuleFixer) => Rule.Fix | null {
+    const { parent } = alternative
+    if (parent.alternatives.length === 1) {
+        // We have to remove the string disjunction instead.
+        // We replace it with `[]` to avoid situations like `[&\q{a}&]` -> `[&&]`
+        return context.fixReplaceNode(parent, "[]")
+    }
+
+    return context.fixReplaceNode(parent, () => {
+        let { start, end } = alternative
+        if (parent.alternatives[0] === alternative) {
+            end++
+        } else {
+            start--
+        }
+
+        const before = parent.raw.slice(0, start - parent.start)
+        const after = parent.raw.slice(end - parent.start)
+
+        return before + after
+    })
+}
+
 /**
  * Check the siblings to see if the regex doesn't change when unwrapped.
  */
diff --git a/lib/utils/partial-parser.ts b/lib/utils/partial-parser.ts
index 9817d02..6986c71 100644
--- a/lib/utils/partial-parser.ts
+++ b/lib/utils/partial-parser.ts
@@ -9,9 +9,15 @@ import { JS } from "refa"
 import type { AST } from "@eslint-community/regexpp"
 import { assertNever } from "./util"
 
-export type NestedAlternative = AST.Alternative | AST.CharacterClassElement
+export type NestedAlternative =
+    | AST.Alternative
+    | AST.CharacterClassElement
+    | AST.StringAlternative
 
 class Context {
+    /**
+     * The ancestor nodes of the alternative + the alternative itself.
+     */
     public readonly ancestors: ReadonlySet<AST.Node>
 
     public readonly alternative: NestedAlternative
@@ -27,15 +33,14 @@ class Context {
     }
 }
 
+type Parsable = JS.ParsableElement | AST.CharacterClassElement
+
 export class PartialParser {
     private readonly parser: JS.Parser
 
     private readonly options: JS.ParseOptions
 
-    private readonly nativeCache = new WeakMap<
-        JS.ParsableElement,
-        NoParent<Element>
-    >()
+    private readonly nativeCache = new WeakMap<Parsable, NoParent<Element>>()
 
     public constructor(parser: JS.Parser, options: JS.ParseOptions = {}) {
         this.parser = parser
@@ -117,8 +122,34 @@ export class PartialParser {
         }
     }
 
+    private parseStringAlternatives(
+        alternatives: readonly AST.StringAlternative[],
+        context: Context,
+    ): NoParent<Concatenation>[] {
+        const ancestor = alternatives.find((a) => context.ancestors.has(a))
+        if (ancestor) {
+            return [this.parseStringAlternative(ancestor)]
+        }
+
+        return alternatives.map((a) => this.parseStringAlternative(a))
+    }
+
+    private parseStringAlternative(
+        alternative: AST.StringAlternative,
+    ): NoParent<Concatenation> {
+        return {
+            type: "Concatenation",
+            elements: alternative.elements.map((e) =>
+                this.nativeParseElement(e),
+            ),
+        }
+    }
+
     private parseElement(
-        element: AST.Element,
+        element:
+            | AST.Element
+            | AST.CharacterClassRange
+            | AST.ClassStringDisjunction,
         context: Context,
     ): NoParent<Element> {
         if (!context.ancestors.has(element)) {
@@ -130,11 +161,29 @@ export class PartialParser {
             case "Backreference":
             case "Character":
             case "CharacterSet":
+            case "ExpressionCharacterClass":
+                return this.nativeParseElement(element)
+
+            case "CharacterClassRange":
+                if (context.alternative === element.min) {
+                    return this.nativeParseElement(context.alternative)
+                } else if (context.alternative === element.max) {
+                    return this.nativeParseElement(context.alternative)
+                }
                 return this.nativeParseElement(element)
 
             case "CharacterClass":
                 return this.parseCharacterClass(element, context)
 
+            case "ClassStringDisjunction":
+                return {
+                    type: "Alternation",
+                    alternatives: this.parseStringAlternatives(
+                        element.alternatives,
+                        context,
+                    ),
+                }
+
             case "Group":
             case "CapturingGroup":
                 return {
@@ -175,8 +224,6 @@ export class PartialParser {
             }
 
             default:
-                // FIXME: TS Error
-                // @ts-expect-error -- FIXME
                 throw assertNever(element)
         }
     }
@@ -193,22 +240,17 @@ export class PartialParser {
             return this.nativeParseElement(cc)
         }
 
-        if (context.alternative.type === "CharacterClassRange") {
-            const range: CharRange = {
-                min: context.alternative.min.value,
-                max: context.alternative.max.value,
-            }
-            return {
-                type: "CharacterClass",
-                characters: JS.createCharSet([range], this.parser.flags),
+        for (const e of cc.elements) {
+            if (context.ancestors.has(e)) {
+                return this.parseElement(e, context)
             }
         }
-        // FIXME: TS Error
-        // @ts-expect-error -- FIXME
-        return this.nativeParseElement(context.alternative)
+
+        // this means that cc === context.alternative
+        return this.nativeParseElement(cc)
     }
 
-    private nativeParseElement(element: JS.ParsableElement): NoParent<Element> {
+    private nativeParseElement(element: Parsable): NoParent<Element> {
         let cached = this.nativeCache.get(element)
         if (!cached) {
             cached = this.nativeParseElementUncached(element)
@@ -217,9 +259,25 @@ export class PartialParser {
         return cached
     }
 
-    private nativeParseElementUncached(
-        element: JS.ParsableElement,
-    ): NoParent<Element> {
+    private nativeParseElementUncached(element: Parsable): NoParent<Element> {
+        if (element.type === "CharacterClassRange") {
+            const range: CharRange = {
+                min: element.min.value,
+                max: element.max.value,
+            }
+            return {
+                type: "CharacterClass",
+                characters: JS.createCharSet([range], this.parser.flags),
+            }
+        } else if (element.type === "ClassStringDisjunction") {
+            return {
+                type: "Alternation",
+                alternatives: element.alternatives.map((a) =>
+                    this.parseStringAlternative(a),
+                ),
+            }
+        }
+
         const { expression } = this.parser.parseElement(element, this.options)
 
         if (expression.alternatives.length === 1) {
diff --git a/package-lock.json b/package-lock.json
index 671d116..ec8be2f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "eslint-plugin-regexp",
-    "version": "2.0.0-next.6",
+    "version": "2.0.0-next.7",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "eslint-plugin-regexp",
-            "version": "2.0.0-next.6",
+            "version": "2.0.0-next.7",
             "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
@@ -46,7 +46,7 @@
                 "eslint-plugin-regexp": "~1.15.0",
                 "eslint-plugin-vue": "^9.0.0",
                 "eslint-plugin-yml": "^1.0.0",
-                "markdownlint-cli": "^0.36.0",
+                "markdownlint-cli": "^0.37.0",
                 "mocha": "^10.0.0",
                 "mocha-chai-jest-snapshot": "^1.1.3",
                 "npm-run-all": "^4.1.5",
@@ -7665,9 +7665,9 @@
             }
         },
         "node_modules/markdownlint": {
-            "version": "0.30.0",
-            "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.30.0.tgz",
-            "integrity": "sha512-nInuFvI/rEzanAOArW5490Ez4EYpB5ODqVM0mcDYCPx9DKJWCQqCgejjiCvbSeE7sjbDscVtZmwr665qpF5xGA==",
+            "version": "0.31.1",
+            "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.31.1.tgz",
+            "integrity": "sha512-CKMR2hgcIBrYlIUccDCOvi966PZ0kJExDrUi1R+oF9PvqQmCrTqjOsgIvf2403OmJ+CWomuzDoylr6KbuMyvHA==",
             "dev": true,
             "dependencies": {
                 "markdown-it": "13.0.1",
@@ -7678,9 +7678,9 @@
             }
         },
         "node_modules/markdownlint-cli": {
-            "version": "0.36.0",
-            "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.36.0.tgz",
-            "integrity": "sha512-h4WdqOam3+QOVOcJSOQuG8KvvN8dlS0OiJhbPwYWBk7VMZR40UtSSMIOpSP5B4EHPHg3W3ILSQUvqg1HNpTCxA==",
+            "version": "0.37.0",
+            "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.37.0.tgz",
+            "integrity": "sha512-hNKAc0bWBBuVhJbSWbUhRzavstiB4o1jh3JeSpwC4/dt6eJ54lRfYHRxVdzVp4qGWBKbeE6Pg490PFEfrKjqSg==",
             "dev": true,
             "dependencies": {
                 "commander": "~11.0.0",
@@ -7689,7 +7689,7 @@
                 "ignore": "~5.2.4",
                 "js-yaml": "^4.1.0",
                 "jsonc-parser": "~3.2.0",
-                "markdownlint": "~0.30.0",
+                "markdownlint": "~0.31.1",
                 "minimatch": "~9.0.3",
                 "run-con": "~1.3.2"
             },
@@ -7735,9 +7735,9 @@
             }
         },
         "node_modules/markdownlint-cli/node_modules/glob": {
-            "version": "10.3.4",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
-            "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
+            "version": "10.3.5",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.5.tgz",
+            "integrity": "sha512-bYUpUD7XDEHI4Q2O5a7PXGvyw4deKR70kHiDxzQbe925wbZknhOzUt2xBgTkYL6RBcVeXYuD9iNYeqoWbBZQnA==",
             "dev": true,
             "dependencies": {
                 "foreground-child": "^3.1.0",
@@ -16973,9 +16973,9 @@
             "dev": true
         },
         "markdownlint": {
-            "version": "0.30.0",
-            "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.30.0.tgz",
-            "integrity": "sha512-nInuFvI/rEzanAOArW5490Ez4EYpB5ODqVM0mcDYCPx9DKJWCQqCgejjiCvbSeE7sjbDscVtZmwr665qpF5xGA==",
+            "version": "0.31.1",
+            "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.31.1.tgz",
+            "integrity": "sha512-CKMR2hgcIBrYlIUccDCOvi966PZ0kJExDrUi1R+oF9PvqQmCrTqjOsgIvf2403OmJ+CWomuzDoylr6KbuMyvHA==",
             "dev": true,
             "requires": {
                 "markdown-it": "13.0.1",
@@ -16983,9 +16983,9 @@
             }
         },
         "markdownlint-cli": {
-            "version": "0.36.0",
-            "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.36.0.tgz",
-            "integrity": "sha512-h4WdqOam3+QOVOcJSOQuG8KvvN8dlS0OiJhbPwYWBk7VMZR40UtSSMIOpSP5B4EHPHg3W3ILSQUvqg1HNpTCxA==",
+            "version": "0.37.0",
+            "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.37.0.tgz",
+            "integrity": "sha512-hNKAc0bWBBuVhJbSWbUhRzavstiB4o1jh3JeSpwC4/dt6eJ54lRfYHRxVdzVp4qGWBKbeE6Pg490PFEfrKjqSg==",
             "dev": true,
             "requires": {
                 "commander": "~11.0.0",
@@ -16994,7 +16994,7 @@
                 "ignore": "~5.2.4",
                 "js-yaml": "^4.1.0",
                 "jsonc-parser": "~3.2.0",
-                "markdownlint": "~0.30.0",
+                "markdownlint": "~0.31.1",
                 "minimatch": "~9.0.3",
                 "run-con": "~1.3.2"
             },
@@ -17025,9 +17025,9 @@
                     }
                 },
                 "glob": {
-                    "version": "10.3.4",
-                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
-                    "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
+                    "version": "10.3.5",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.5.tgz",
+                    "integrity": "sha512-bYUpUD7XDEHI4Q2O5a7PXGvyw4deKR70kHiDxzQbe925wbZknhOzUt2xBgTkYL6RBcVeXYuD9iNYeqoWbBZQnA==",
                     "dev": true,
                     "requires": {
                         "foreground-child": "^3.1.0",
diff --git a/package.json b/package.json
index 42df5b8..133c91a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "eslint-plugin-regexp",
-    "version": "2.0.0-next.6",
+    "version": "2.0.0-next.7",
     "description": "ESLint plugin for finding RegExp mistakes and RegExp style guide violations.",
     "engines": {
         "node": "^18 || >=20"
@@ -87,7 +87,7 @@
         "eslint-plugin-regexp": "~1.15.0",
         "eslint-plugin-vue": "^9.0.0",
         "eslint-plugin-yml": "^1.0.0",
-        "markdownlint-cli": "^0.36.0",
+        "markdownlint-cli": "^0.37.0",
         "mocha": "^10.0.0",
         "mocha-chai-jest-snapshot": "^1.1.3",
         "npm-run-all": "^4.1.5",
diff --git a/tools/update-docs.ts b/tools/update-docs.ts
index db78776..6d55148 100644
--- a/tools/update-docs.ts
+++ b/tools/update-docs.ts
@@ -16,9 +16,10 @@ const ROOT = path.resolve(__dirname, "../docs/rules")
 function pickSince(content: string): string | null | Promise<string> {
     const fileIntro = /^---\n(?<content>.*\n)+---\n*/u.exec(content)
     if (fileIntro) {
-        const since = /since: "?(?<version>v\d+\.\d+\.\d+)"?/u.exec(
-            fileIntro.groups!.content,
-        )
+        const since =
+            /since: "?(?<version>v\d+\.\d+\.\d+(?:[^\d\n"][^\n"]*)?)"?/u.exec(
+                fileIntro.groups!.content,
+            )
         if (since) {
             return since.groups!.version
         }


Test patch:
diff --git a/tests/lib/rules/no-dupe-disjunctions.ts b/tests/lib/rules/no-dupe-disjunctions.ts
index 71521e0..cf0128d 100644
--- a/tests/lib/rules/no-dupe-disjunctions.ts
+++ b/tests/lib/rules/no-dupe-disjunctions.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-dupe-disjunctions"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -19,6 +19,7 @@ tester.run("no-dupe-disjunctions", rule as any, {
         `/(?:js|json)?abc/`,
         `/<("[^"]*"|'[^']*'|[^'">])*>/g`,
         `/c+|[a-f]/`,
+        `/c+|[a-f]/v`,
         String.raw`/b+(?:\w+|[+-]?\d+)/`,
         String.raw`/\d*\.\d+_|\d+\.\d*_/`,
         String.raw`/\d*\.\d+|\d+\.\d*/`,
@@ -149,6 +150,12 @@ tester.run("no-dupe-disjunctions", rule as any, {
                 "Unexpected duplicate alternative. This alternative can be removed.",
             ],
         },
+        {
+            code: `/((?:ab|ba)|(?:ab|ba))/v`,
+            errors: [
+                "Unexpected duplicate alternative. This alternative can be removed.",
+            ],
+        },
         {
             code: `/((?:ab|ba)|(?:ba|ab))/`,
             errors: [
@@ -514,6 +521,12 @@ tester.run("no-dupe-disjunctions", rule as any, {
                 "Unexpected useless alternative. This alternative is a strict subset of 'A*' and can be removed.",
             ],
         },
+        {
+            code: String.raw`/[\q{a|bb}]|bb/v`,
+            errors: [
+                "Unexpected useless alternative. This alternative is a strict subset of '[\\q{a|bb}]' and can be removed.",
+            ],
+        },
         {
             // reportExponentialBacktracking: 'potential'
             code: String.raw`
diff --git a/tests/lib/rules/prefer-predefined-assertion.ts b/tests/lib/rules/prefer-predefined-assertion.ts
index 8c26bf2..3aaf592 100644
--- a/tests/lib/rules/prefer-predefined-assertion.ts
+++ b/tests/lib/rules/prefer-predefined-assertion.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/prefer-predefined-assertion"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("prefer-predefined-assertion", rule as any, {
-    valid: [String.raw`/a(?=\W)/`],
+    valid: [String.raw`/a(?=\W)/`, String.raw`/a(?=\W)/v`],
     invalid: [
         {
             code: String.raw`/a(?=\w)/`,
@@ -68,6 +68,11 @@ tester.run("prefer-predefined-assertion", rule as any, {
                 "This lookaround assertion can be replaced with a negated word boundary assertion ('\\B').",
             ],
         },
+        {
+            code: String.raw`/.(?<!\W)a/v`,
+            output: String.raw`/.\Ba/v`,
+            errors: 1,
+        },
 
         {
             code: String.raw`/a+(?!\w)(?:\s|bc+)+/`,
diff --git a/tests/lib/rules/sort-character-class-elements.ts b/tests/lib/rules/sort-character-class-elements.ts
index 1ef4681..5c672fa 100644
--- a/tests/lib/rules/sort-character-class-elements.ts
+++ b/tests/lib/rules/sort-character-class-elements.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/sort-character-class-elements"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -38,6 +38,9 @@ tester.run("sort-character-class-elements", rule as any, {
             code: String.raw`/[\p{ASCII}a]/u`,
             options: [{ order: [] }],
         },
+        String.raw`/[a\q{a}[a][a--b]]/v`,
+        String.raw`/[\q{a}\q{b}\q{c}]/v`,
+        String.raw`/[\q{aa}\q{ab}\q{ac}]/v`,
     ],
     invalid: [
         {
@@ -230,5 +233,68 @@ tester.run("sort-character-class-elements", rule as any, {
                 "Expected character class elements to be in ascending order. '\t' should be before ' '.",
             ],
         },
+        {
+            code: String.raw`/[[a--b][a]\q{a}a]/v`,
+            output: String.raw`/[\q{a}[a--b][a]a]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. '\\q{a}' should be before '[a--b]'.",
+                "Expected character class elements to be in ascending order. 'a' should be before '[a--b]'.",
+            ],
+        },
+        {
+            code: String.raw`/[\q{a}[a--b][a]a]/v`,
+            output: String.raw`/[a\q{a}[a--b][a]]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. 'a' should be before '\\q{a}'.",
+            ],
+        },
+        {
+            code: String.raw`/[[b--c][a]]/v`,
+            output: String.raw`/[[a][b--c]]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. '[a]' should be before '[b--c]'.",
+            ],
+        },
+        {
+            code: String.raw`/[[a]\q{a}]/v; /[\q{a}a]/v; /[[b-c]\q{a}]/v; /[[b-c][a]]/v;`,
+            output: String.raw`/[\q{a}[a]]/v; /[a\q{a}]/v; /[\q{a}[b-c]]/v; /[[a][b-c]]/v;`,
+            options: [{ order: [] }],
+            errors: [
+                "Expected character class elements to be in ascending order. '\\q{a}' should be before '[a]'.",
+                "Expected character class elements to be in ascending order. 'a' should be before '\\q{a}'.",
+                "Expected character class elements to be in ascending order. '\\q{a}' should be before '[b-c]'.",
+                "Expected character class elements to be in ascending order. '[a]' should be before '[b-c]'.",
+            ],
+        },
+        {
+            code: String.raw`/[\q{c}\q{b}\q{a}]/v`,
+            output: String.raw`/[\q{b}\q{c}\q{a}]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. '\\q{b}' should be before '\\q{c}'.",
+                "Expected character class elements to be in ascending order. '\\q{a}' should be before '\\q{c}'.",
+            ],
+        },
+        {
+            code: String.raw`/[\q{b}\q{c}\q{a}]/v`,
+            output: String.raw`/[\q{a}\q{b}\q{c}]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. '\\q{a}' should be before '\\q{b}'.",
+            ],
+        },
+        {
+            code: String.raw`/[\q{ac}\q{ab}\q{aa}]/v`,
+            output: String.raw`/[\q{ab}\q{ac}\q{aa}]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. '\\q{ab}' should be before '\\q{ac}'.",
+                "Expected character class elements to be in ascending order. '\\q{aa}' should be before '\\q{ac}'.",
+            ],
+        },
+        {
+            code: String.raw`/[\q{ab}\q{ac}\q{aa}]/v`,
+            output: String.raw`/[\q{aa}\q{ab}\q{ac}]/v`,
+            errors: [
+                "Expected character class elements to be in ascending order. '\\q{aa}' should be before '\\q{ab}'.",
+            ],
+        },
     ],
 })