Repository: ota-meshi/eslint-plugin-regexp
Pull request #642 (2023-10-03 14:58:21)

Use Intl.Segmenter instead of grapheme-splitter

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

Latest commit: 1945327ffebf2dbf033639efbbdd6d8e49552481

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/segmenter?filename=.changeset/hungry-coins-help.md&value=---%0A%22eslint-plugin-regexp%22%3A%20patch%0A---%0A%0AUse%20Intl.Segmenter%20instead%20of%20grapheme-splitter%0A)



Patch:
diff --git a/.changeset/hungry-coins-help.md b/.changeset/hungry-coins-help.md
new file mode 100644
index 0000000..2f901f0
--- /dev/null
+++ b/.changeset/hungry-coins-help.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Use Intl.Segmenter instead of grapheme-splitter
diff --git a/.changeset/quick-ties-bake.md b/.changeset/quick-ties-bake.md
new file mode 100644
index 0000000..6a0a783
--- /dev/null
+++ b/.changeset/quick-ties-bake.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": patch
+---
+
+fix: autofix for incorrect no-escaping in `regexp/no-useless-string-literal`
diff --git a/README.md b/README.md
index 63f64d0..a780a50 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ This ESLint plugin provides linting rules relate to better ways to help you avoi
 - Find the wrong usage of regular expressions, and their hints.
 - Enforces a consistent style of regular expressions.
 - Find hints for writing optimized regular expressions.
+- 80 plugin rules for regular expression syntax and features.
 
 You can check on the [Online DEMO](https://ota-meshi.github.io/eslint-plugin-regexp/playground/).
 
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index d93be20..11e3179 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -11,20 +11,42 @@ if (typeof window !== "undefined") {
 }
 import type { Theme } from "vitepress"
 import DefaultTheme from "vitepress/theme"
-// @ts-expect-error -- ignore
 import Layout from "./Layout.vue"
-// @ts-expect-error -- ignore
-import ESLintCodeBlock from "./components/eslint-code-block.vue"
-// @ts-expect-error -- ignore
-import PlaygroundBlock from "./components/playground-block.vue"
 
 const theme: Theme = {
     ...DefaultTheme,
     Layout,
-    enhanceApp(ctx) {
+    async enhanceApp(ctx) {
         DefaultTheme.enhanceApp(ctx)
+
+        if (typeof Intl.Segmenter === "undefined") {
+            await setupIntlSegmenter()
+        }
+
+        const ESLintCodeBlock = await import(
+            "./components/eslint-code-block.vue"
+        ).then((m) => m.default ?? m)
+        const PlaygroundBlock = await import(
+            "./components/playground-block.vue"
+        ).then((m) => m.default ?? m)
         ctx.app.component("eslint-code-block", ESLintCodeBlock)
         ctx.app.component("playground-block", PlaygroundBlock)
     },
 }
 export default theme
+
+// We can remove this polyfill once Firefox supports Intl.Segmenter.
+async function setupIntlSegmenter() {
+    // For Firefox
+    const [{ createIntlSegmenterPolyfill }, breakIteratorUrl] =
+        await Promise.all([
+            import("intl-segmenter-polyfill"),
+            import(
+                // @ts-expect-error -- polyfill
+                "intl-segmenter-polyfill/dist/break_iterator.wasm?url"
+            ).then((m) => m.default ?? m),
+        ])
+
+    // @ts-expect-error -- polyfill
+    Intl.Segmenter = await createIntlSegmenterPolyfill(fetch(breakIteratorUrl))
+}
diff --git a/docs/index.md b/docs/index.md
index 4497e88..baa4b4e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -23,6 +23,7 @@ This ESLint plugin provides linting rules relate to better ways to help you avoi
 - Find the wrong usage of regular expressions, and their hints.
 - Enforces a consistent style of regular expressions.
 - Find hints for writing optimized regular expressions.
+- 80 plugin rules for regular expression syntax and features.
 
 You can check on the [Online DEMO](./playground/index.md).
 
diff --git a/lib/rules/no-misleading-unicode-character.ts b/lib/rules/no-misleading-unicode-character.ts
index 80c5f57..8ecf87f 100644
--- a/lib/rules/no-misleading-unicode-character.ts
+++ b/lib/rules/no-misleading-unicode-character.ts
@@ -1,7 +1,6 @@
 import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
 import type { RegExpContext } from "../utils"
 import { isEscapeSequence, createRule, defineRegexpVisitor } from "../utils"
-import GraphemeSplitter from "grapheme-splitter"
 import type { ReadonlyFlags } from "regexp-ast-analysis"
 import { mention, mentionChar } from "../utils/mention"
 import type {
@@ -12,7 +11,7 @@ import type {
 import type { PatternRange } from "../utils/ast-utils/pattern-source"
 import type { Rule } from "eslint"
 
-const splitter = new GraphemeSplitter()
+const segmenter = new Intl.Segmenter()
 
 /** Returns whether the given string starts with a valid surrogate pair. */
 function startsWithSurrogate(s: string): boolean {
@@ -66,10 +65,10 @@ function getGraphemeBeforeQuant(quant: Quantifier): string {
         quant.element.end - alt.start,
     )
 
-    const graphemes = splitter.splitGraphemes(before)
-    const grapheme = graphemes[graphemes.length - 1]
+    const segments = [...segmenter.segment(before)]
+    const segment = segments[segments.length - 1]
 
-    return grapheme
+    return segment.segment
 }
 
 interface GraphemeProblem {
@@ -86,7 +85,7 @@ function getGraphemeProblems(
     cc: CharacterClass,
     flags: ReadonlyFlags,
 ): GraphemeProblem[] {
-    let offset = cc.negate ? 2 : 1
+    const offset = cc.negate ? 2 : 1
 
     const ignoreElements = cc.elements.filter(
         (element) =>
@@ -95,14 +94,15 @@ function getGraphemeProblems(
             element.type === "ClassStringDisjunction",
     )
 
-    const graphemes = splitter.splitGraphemes(cc.raw.slice(offset, -1))
     const problems: GraphemeProblem[] = []
 
-    for (const grapheme of graphemes) {
-        const problem = getProblem(grapheme, flags)
+    for (const { segment, index } of segmenter.segment(
+        cc.raw.slice(offset, -1),
+    )) {
+        const problem = getProblem(segment, flags)
         if (problem !== null) {
-            const start = offset + cc.start
-            const end = start + grapheme.length
+            const start = offset + index + cc.start
+            const end = start + segment.length
 
             if (
                 ignoreElements.some(
@@ -113,7 +113,7 @@ function getGraphemeProblems(
             }
 
             problems.push({
-                grapheme,
+                grapheme: segment,
                 problem,
                 start,
                 end,
@@ -122,7 +122,6 @@ function getGraphemeProblems(
                 ),
             })
         }
-        offset += grapheme.length
     }
 
     return problems
diff --git a/lib/rules/no-useless-string-literal.ts b/lib/rules/no-useless-string-literal.ts
index f4243a4..232d983 100644
--- a/lib/rules/no-useless-string-literal.ts
+++ b/lib/rules/no-useless-string-literal.ts
@@ -43,7 +43,11 @@ export default createRule("no-useless-string-literal", {
                                     .join("|")
                                 if (!alternativesText.length) {
                                     const escape =
-                                        isNeedEscapedInCharacterClass(
+                                        isNeedEscapeForAdjacentPreviousCharacter(
+                                            csdNode,
+                                            saNode,
+                                        ) ||
+                                        isNeedEscapeForAdjacentNextCharacter(
                                             csdNode,
                                             saNode,
                                         )
@@ -60,12 +64,13 @@ export default createRule("no-useless-string-literal", {
                                         saNode.raw === "^" ? "\\" : ""
                                     return String.raw`[${escape}${saNode.raw}\q{${alternativesText}}]`
                                 }
-                                const escape = isNeedEscapedInCharacterClass(
-                                    csdNode,
-                                    saNode,
-                                )
-                                    ? "\\"
-                                    : ""
+                                const escape =
+                                    isNeedEscapeForAdjacentPreviousCharacter(
+                                        csdNode,
+                                        saNode,
+                                    )
+                                        ? "\\"
+                                        : ""
                                 return String.raw`${escape}${saNode.raw}\q{${alternativesText}}`
                             }),
                         })
@@ -74,9 +79,10 @@ export default createRule("no-useless-string-literal", {
             }
 
             /**
-             * Checks whether an escape is required if the given character when placed before a \q{...}
+             * Checks whether the given character requires escaping
+             * when adjacent to the previous character.
              */
-            function isNeedEscapedInCharacterClass(
+            function isNeedEscapeForAdjacentPreviousCharacter(
                 disjunction: ClassStringDisjunction,
                 character: StringAlternative,
             ) {
@@ -97,6 +103,23 @@ export default createRule("no-useless-string-literal", {
                     disjunction.parent.start === disjunction.start - 1
                 )
             }
+
+            /**
+             * Checks whether the given character requires escaping
+             * when adjacent to the next character.
+             */
+            function isNeedEscapeForAdjacentNextCharacter(
+                disjunction: ClassStringDisjunction,
+                character: StringAlternative,
+            ) {
+                const char = character.raw
+                // Avoid [\q{&}&&A] => [&&&A]
+                return (
+                    RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
+                    // The next character is the same
+                    pattern[disjunction.end] === char
+                )
+            }
         }
 
         return defineRegexpVisitor(context, {
diff --git a/lib/utils/extract-capturing-group-references.ts b/lib/utils/extract-capturing-group-references.ts
index 1605639..b439659 100644
--- a/lib/utils/extract-capturing-group-references.ts
+++ b/lib/utils/extract-capturing-group-references.ts
@@ -184,6 +184,13 @@ const WELL_KNOWN_ARRAY_METHODS: {
     flat: {},
     // ES2022
     at: { result: "element" },
+    // ES2023
+    findLast: { elementParameters: [0], result: "element" },
+    findLastIndex: { elementParameters: [0] },
+    toReversed: { result: "array" },
+    toSorted: { elementParameters: [0, 1], result: "array" },
+    toSpliced: { result: "array" },
+    with: { result: "array" },
 }
 
 /**
diff --git a/lib/utils/type-tracker/type-data/array.ts b/lib/utils/type-tracker/type-data/array.ts
index f06acf9..123bb5a 100644
--- a/lib/utils/type-tracker/type-data/array.ts
+++ b/lib/utils/type-tracker/type-data/array.ts
@@ -255,6 +255,13 @@ const getPrototypes = cache(() => {
         flat: RETURN_UNKNOWN_ARRAY,
         // ES2022
         at: RETURN_ARRAY_ELEMENT, // element
+        // ES2023
+        findLast: RETURN_ARRAY_ELEMENT, // element
+        findLastIndex: RETURN_NUMBER,
+        toReversed: RETURN_SELF,
+        toSorted: RETURN_SELF,
+        toSpliced: RETURN_SELF,
+        with: RETURN_SELF,
 
         length: NUMBER,
         0: null, // element
diff --git a/lib/utils/type-tracker/type-data/object.ts b/lib/utils/type-tracker/type-data/object.ts
index f7fbb08..43a41ac 100644
--- a/lib/utils/type-tracker/type-data/object.ts
+++ b/lib/utils/type-tracker/type-data/object.ts
@@ -196,6 +196,8 @@ export function buildObjectConstructor(): TypeGlobalFunction {
         getOwnPropertyDescriptors: null,
         // ES2019
         fromEntries: null,
+        // ES2022
+        hasOwn: RETURN_BOOLEAN,
 
         prototype: null,
     })
diff --git a/lib/utils/type-tracker/type-data/regexp.ts b/lib/utils/type-tracker/type-data/regexp.ts
index b837627..b292f7a 100644
--- a/lib/utils/type-tracker/type-data/regexp.ts
+++ b/lib/utils/type-tracker/type-data/regexp.ts
@@ -103,6 +103,8 @@ const getPrototypes: () => {
         unicode: BOOLEAN, // prop
         // ES2018
         dotAll: BOOLEAN, // prop
+        // ES2022
+        hasIndices: BOOLEAN, // prop
 
         [Symbol.match]: null,
         [Symbol.replace]: null,
diff --git a/lib/utils/type-tracker/type-data/string.ts b/lib/utils/type-tracker/type-data/string.ts
index e5ed41f..bef4b0d 100644
--- a/lib/utils/type-tracker/type-data/string.ts
+++ b/lib/utils/type-tracker/type-data/string.ts
@@ -97,7 +97,7 @@ const getPrototypes: () => {
         trim: RETURN_STRING,
         substr: RETURN_STRING,
         valueOf: RETURN_STRING,
-        // ES2051
+        // ES2015
         codePointAt: RETURN_NUMBER,
         includes: RETURN_BOOLEAN,
         endsWith: RETURN_BOOLEAN,
@@ -128,6 +128,8 @@ const getPrototypes: () => {
         trimEnd: RETURN_STRING,
         // ES2020
         matchAll: null, // IterableIterator<RegExpMatchArray>
+        // ES2021
+        replaceAll: RETURN_STRING,
         // ES2022
         at: RETURN_STRING,
 
diff --git a/package-lock.json b/package-lock.json
index ee54e71..82a5c3f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,17 @@
 {
     "name": "eslint-plugin-regexp",
-    "version": "2.0.0-next.10",
+    "version": "2.0.0-next.12",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "eslint-plugin-regexp",
-            "version": "2.0.0-next.10",
+            "version": "2.0.0-next.12",
             "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.9.1",
                 "comment-parser": "^1.4.0",
-                "grapheme-splitter": "^1.0.4",
                 "jsdoctypeparser": "^9.0.0",
                 "refa": "^0.12.1",
                 "regexp-ast-analysis": "^0.7.1",
@@ -46,6 +45,7 @@
                 "eslint-plugin-regexp": "~1.15.0",
                 "eslint-plugin-vue": "^9.0.0",
                 "eslint-plugin-yml": "^1.0.0",
+                "intl-segmenter-polyfill": "^0.4.4",
                 "markdownlint-cli": "^0.37.0",
                 "mocha": "^10.0.0",
                 "mocha-chai-jest-snapshot": "^1.1.3",
@@ -5738,6 +5738,12 @@
             "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
             "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
         },
+        "node_modules/fast-text-encoding": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
+            "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==",
+            "dev": true
+        },
         "node_modules/fastest-levenshtein": {
             "version": "1.0.16",
             "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@@ -6197,7 +6203,8 @@
         "node_modules/grapheme-splitter": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
-            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
+            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+            "dev": true
         },
         "node_modules/graphemer": {
             "version": "1.4.0",
@@ -6466,6 +6473,15 @@
                 "node": ">= 0.4"
             }
         },
+        "node_modules/intl-segmenter-polyfill": {
+            "version": "0.4.4",
+            "resolved": "https://registry.npmjs.org/intl-segmenter-polyfill/-/intl-segmenter-polyfill-0.4.4.tgz",
+            "integrity": "sha512-dIOcmvH+Q1WYGkjMqxPfaCgHEwOegH5UPcd/LLeaeY8aguHadC46MzGb40q8C1LrsuyJxJGKeKqoVtIh9ADRXQ==",
+            "dev": true,
+            "dependencies": {
+                "fast-text-encoding": "^1.0.2"
+            }
+        },
         "node_modules/is-arguments": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -15544,6 +15560,12 @@
             "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
             "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
         },
+        "fast-text-encoding": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
+            "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==",
+            "dev": true
+        },
         "fastest-levenshtein": {
             "version": "1.0.16",
             "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@@ -15882,7 +15904,8 @@
         "grapheme-splitter": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
-            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
+            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+            "dev": true
         },
         "graphemer": {
             "version": "1.4.0",
@@ -16074,6 +16097,15 @@
                 "side-channel": "^1.0.4"
             }
         },
+        "intl-segmenter-polyfill": {
+            "version": "0.4.4",
+            "resolved": "https://registry.npmjs.org/intl-segmenter-polyfill/-/intl-segmenter-polyfill-0.4.4.tgz",
+            "integrity": "sha512-dIOcmvH+Q1WYGkjMqxPfaCgHEwOegH5UPcd/LLeaeY8aguHadC46MzGb40q8C1LrsuyJxJGKeKqoVtIh9ADRXQ==",
+            "dev": true,
+            "requires": {
+                "fast-text-encoding": "^1.0.2"
+            }
+        },
         "is-arguments": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
diff --git a/package.json b/package.json
index b61e170..597ea2a 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
         "eslint-plugin-regexp": "~1.15.0",
         "eslint-plugin-vue": "^9.0.0",
         "eslint-plugin-yml": "^1.0.0",
+        "intl-segmenter-polyfill": "^0.4.4",
         "markdownlint-cli": "^0.37.0",
         "mocha": "^10.0.0",
         "mocha-chai-jest-snapshot": "^1.1.3",
@@ -105,7 +106,6 @@
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.9.1",
         "comment-parser": "^1.4.0",
-        "grapheme-splitter": "^1.0.4",
         "jsdoctypeparser": "^9.0.0",
         "refa": "^0.12.1",
         "regexp-ast-analysis": "^0.7.1",
diff --git a/tools/update-readme.ts b/tools/update-readme.ts
index 11d2813..09cc22e 100644
--- a/tools/update-readme.ts
+++ b/tools/update-readme.ts
@@ -1,8 +1,16 @@
 import path from "path"
 import fs from "fs"
+import { rules } from "./lib/load-rules"
 
 const readmeFilePath = path.resolve(__dirname, "../README.md")
-const readme = fs.readFileSync(readmeFilePath, "utf8")
+const readme = fs
+    .readFileSync(readmeFilePath, "utf8")
+    .replace(
+        /\d{1,4}plugin rules/u,
+        `${rules.filter((rule) => !rule.meta.deprecated).length} plugin rules`,
+    )
+
+fs.writeFileSync(readmeFilePath, readme)
 
 const docsReadmeFilePath = path.resolve(__dirname, "../docs/index.md")
 
diff --git a/tsconfig.json b/tsconfig.json
index 934164d..cb90038 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
         "target": "es2015",
         "module": "Node16",
         "moduleResolution": "Node16",
-        "lib": ["es2020"],
+        "lib": ["es2023"],
         "allowJs": true,
         "checkJs": true,
         "outDir": "./dist",


Test patch:
diff --git a/tests/lib/rules/confusing-quantifier.ts b/tests/lib/rules/confusing-quantifier.ts
index d6d85c0..ade608e 100644
--- a/tests/lib/rules/confusing-quantifier.ts
+++ b/tests/lib/rules/confusing-quantifier.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/confusing-quantifier"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -15,6 +15,7 @@ tester.run("confusing-quantifier", rule as any, {
         String.raw`/(a|b?)*/`,
         String.raw`/(a?){0,3}/`,
         String.raw`/(a|\b)+/`,
+        String.raw`/[\q{a|b}]+/v`,
     ],
     invalid: [
         {
@@ -39,5 +40,16 @@ tester.run("confusing-quantifier", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[\q{a|}]+/v`,
+            errors: [
+                {
+                    message:
+                        "This quantifier is confusing because its minimum is 1 but it can match the empty string. Maybe replace it with `*` to reflect that it can match the empty string?",
+                    line: 1,
+                    column: 10,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/control-character-escape.ts b/tests/lib/rules/control-character-escape.ts
index 75ba623..1ec093c 100644
--- a/tests/lib/rules/control-character-escape.ts
+++ b/tests/lib/rules/control-character-escape.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/control-character-escape"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -16,6 +16,7 @@ tester.run("control-character-escape", rule as any, {
         String.raw`RegExp("\\0\\t\\n\\v\\f\\r", "i")`,
         "/\\t/",
         "new RegExp('\t')",
+        String.raw`/[\q{\0\t\n\v\f\r}]/v`,
     ],
     invalid: [
         {
@@ -115,5 +116,12 @@ tester.run("control-character-escape", rule as any, {
                 "Unexpected control character escape '\t' (U+0009). Use '\\t' instead.",
             ],
         },
+        {
+            code: String.raw`/[\q{\x00}]/v`,
+            output: String.raw`/[\q{\0}]/v`,
+            errors: [
+                "Unexpected control character escape '\\x00' (U+0000). Use '\\0' instead.",
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/hexadecimal-escape.ts b/tests/lib/rules/hexadecimal-escape.ts
index 104d88a..3d9509c 100644
--- a/tests/lib/rules/hexadecimal-escape.ts
+++ b/tests/lib/rules/hexadecimal-escape.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/hexadecimal-escape"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -29,6 +29,14 @@ tester.run("hexadecimal-escape", rule as any, {
             options: ["never"],
         },
         String.raw`/\cA \cB \cM/`,
+        {
+            code: String.raw`/[\q{\x0a}]/v`,
+            options: ["always"],
+        },
+        {
+            code: String.raw`/[\q{\u000a}]/v`,
+            options: ["never"],
+        },
     ],
     invalid: [
         {
@@ -98,5 +106,23 @@ tester.run("hexadecimal-escape", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[\q{\u000a \u{00000a}}]/v`,
+            output: String.raw`/[\q{\x0a \x0a}]/v`,
+            options: ["always"],
+            errors: [
+                "Expected hexadecimal escape ('\\x0a'), but unicode escape ('\\u000a') is used.",
+                "Expected hexadecimal escape ('\\x0a'), but unicode code point escape ('\\u{00000a}') is used.",
+            ],
+        },
+        {
+            code: String.raw`/[\q{\x0f \xff}]/v`,
+            output: String.raw`/[\q{\u000f \u00ff}]/v`,
+            options: ["never"],
+            errors: [
+                "Unexpected hexadecimal escape ('\\x0f').",
+                "Unexpected hexadecimal escape ('\\xff').",
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/letter-case.ts b/tests/lib/rules/letter-case.ts
index 1d658f0..82e6e86 100644
--- a/tests/lib/rules/letter-case.ts
+++ b/tests/lib/rules/letter-case.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/letter-case"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -49,6 +49,19 @@ tester.run("letter-case", rule as any, {
             code: `/[A-z]/i`,
             options: [{ caseInsensitive: "ignore" }],
         },
+        String.raw`/[\q{ab}]/iv`,
+        {
+            code: String.raw`/[\q{ab}]/iv`,
+            options: [{ caseInsensitive: "lowercase" }],
+        },
+        {
+            code: String.raw`/[\q{AB}]/iv`,
+            options: [{ caseInsensitive: "uppercase" }],
+        },
+        {
+            code: String.raw`/[\q{Ab}]/iv`,
+            options: [{ caseInsensitive: "ignore" }],
+        },
         String.raw`/\u000a/`,
         {
             code: String.raw`/\u000a/`,
@@ -266,5 +279,22 @@ tester.run("letter-case", rule as any, {
             options: [{ unicodeEscape: "lowercase" }],
             errors: [String.raw`'\u000A' is not in lowercase`],
         },
+        {
+            code: String.raw`/[\q{Ab}]/iv`,
+            output: String.raw`/[\q{ab}]/iv`,
+            errors: ["'A' is not in lowercase"],
+        },
+        {
+            code: String.raw`/[\q{Ab}]/iv`,
+            output: String.raw`/[\q{ab}]/iv`,
+            options: [{ caseInsensitive: "lowercase" }],
+            errors: ["'A' is not in lowercase"],
+        },
+        {
+            code: String.raw`/[\q{Ab}]/iv`,
+            output: String.raw`/[\q{AB}]/iv`,
+            options: [{ caseInsensitive: "uppercase" }],
+            errors: ["'b' is not in uppercase"],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-control-character.ts b/tests/lib/rules/no-control-character.ts
index 22518da..c5a9c96 100644
--- a/tests/lib/rules/no-control-character.ts
+++ b/tests/lib/rules/no-control-character.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-control-character"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -102,5 +102,9 @@ tester.run("no-control-character", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[\q{\x1f}]/v`,
+            errors: [{ messageId: "unexpected", suggestions: [] }],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-empty-group.ts b/tests/lib/rules/no-empty-group.ts
index 9632541..2204aff 100644
--- a/tests/lib/rules/no-empty-group.ts
+++ b/tests/lib/rules/no-empty-group.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/no-empty-group"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-empty-group", rule as any, {
-    valid: ["/(a)/", "/(a|)/", "/(?:a|)/"],
+    valid: ["/(a)/", "/(a|)/", "/(?:a|)/", String.raw`/(?:a|[\q{}])/v`],
     invalid: [
         {
             code: "/()/",
diff --git a/tests/lib/rules/no-empty-lookarounds-assertion.ts b/tests/lib/rules/no-empty-lookarounds-assertion.ts
index 0018e7b..4244952 100644
--- a/tests/lib/rules/no-empty-lookarounds-assertion.ts
+++ b/tests/lib/rules/no-empty-lookarounds-assertion.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-empty-lookarounds-assertion"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -20,6 +20,7 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
         "/(?=$|a)/",
         "/(?=\\ba*\\b)/",
         '/b?r(#*)"(?:[^"]|"(?!\\1))*"\\1/',
+        String.raw`/x(?=[\q{a}])/v`,
     ],
     invalid: [
         {
@@ -174,5 +175,14 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/x(?=[\q{}])/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected empty lookahead. It will trivially accept all inputs.",
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-escape-backspace.ts b/tests/lib/rules/no-escape-backspace.ts
index fe261fa..dd8b239 100644
--- a/tests/lib/rules/no-escape-backspace.ts
+++ b/tests/lib/rules/no-escape-backspace.ts
@@ -3,13 +3,19 @@ import rule from "../../../lib/rules/no-escape-backspace"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-escape-backspace", rule as any, {
-    valid: ["/\\b/", "/\\u0008/", "/\\ch/", "/\\cH/"],
+    valid: [
+        "/\\b/",
+        "/\\u0008/",
+        "/\\ch/",
+        "/\\cH/",
+        String.raw`/[\q{\u0008}]/v`,
+    ],
     invalid: [
         {
             code: "/[\\b]/",
@@ -22,5 +28,16 @@ tester.run("no-escape-backspace", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[\q{\b}]/v`,
+            errors: [
+                {
+                    message: "Unexpected '[\\b]'. Use '\\u0008' instead.",
+                    column: 6,
+                    endColumn: 8,
+                    suggestions: [{ output: String.raw`/[\q{\u0008}]/v` }],
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-extra-lookaround-assertions.ts b/tests/lib/rules/no-extra-lookaround-assertions.ts
index 0bb380f..32876ca 100644
--- a/tests/lib/rules/no-extra-lookaround-assertions.ts
+++ b/tests/lib/rules/no-extra-lookaround-assertions.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-extra-lookaround-assertions"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -12,6 +12,7 @@ tester.run("no-extra-lookaround-assertions", rule as any, {
     valid: [
         `console.log('JavaScript'.replace(/Java(?=Script)/u, 'Type'))`,
         `console.log('JavaScript'.replace(/(?<=Java)Script/u, ''))`,
+        String.raw`console.log('JavaScript'.replace(/(?<=[\q{Java}])Script/v, ''))`,
     ],
     invalid: [
         {
@@ -125,5 +126,16 @@ tester.run("no-extra-lookaround-assertions", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`console.log('JavaScript'.replace(/Java(?=Scrip(?=[\q{t}]))/v, 'Type'))`,
+            output: String.raw`console.log('JavaScript'.replace(/Java(?=Scrip[\q{t}])/v, 'Type'))`,
+            errors: [
+                {
+                    message:
+                        "This lookahead assertion is useless and can be inlined.",
+                    column: 47,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-invisible-character.ts b/tests/lib/rules/no-invisible-character.ts
index 974c3dd..72f898b 100644
--- a/tests/lib/rules/no-invisible-character.ts
+++ b/tests/lib/rules/no-invisible-character.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-invisible-character"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -22,6 +22,7 @@ tester.run("no-invisible-character", rule as any, {
         "new RegExp(' ')",
         "new RegExp('a')",
         "new RegExp('[ ]')",
+        String.raw`/[\q{\t}]/v`,
     ],
     invalid: [
         {
@@ -104,5 +105,10 @@ tester.run("no-invisible-character", rule as any, {
                 "Unexpected invisible character. Use '\\u200b' instead.",
             ],
         },
+        {
+            code: `/[\\q{\t}]/v`,
+            output: String.raw`/[\q{\t}]/v`,
+            errors: ["Unexpected invisible character. Use '\\t' instead."],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-lazy-ends.ts b/tests/lib/rules/no-lazy-ends.ts
index 2c5f528..329eea7 100644
--- a/tests/lib/rules/no-lazy-ends.ts
+++ b/tests/lib/rules/no-lazy-ends.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-lazy-ends"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -28,6 +28,8 @@ tester.run("no-lazy-ends", rule as any, {
                 sourceType: "script",
             },
         },
+
+        String.raw`/[\q{ab}]?/v.test(str)`,
     ],
     invalid: [
         {
@@ -200,5 +202,16 @@ tester.run("no-lazy-ends", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[\q{ab|}]??/v.test(str)`,
+            errors: [
+                {
+                    message:
+                        "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
+                    line: 1,
+                    column: 2,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-legacy-features.ts b/tests/lib/rules/no-legacy-features.ts
index 986989b..3a53841 100644
--- a/tests/lib/rules/no-legacy-features.ts
+++ b/tests/lib/rules/no-legacy-features.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-legacy-features"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
diff --git a/tests/lib/rules/no-missing-g-flag.ts b/tests/lib/rules/no-missing-g-flag.ts
index 078fe40..519d85d 100644
--- a/tests/lib/rules/no-missing-g-flag.ts
+++ b/tests/lib/rules/no-missing-g-flag.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-missing-g-flag"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -52,6 +52,11 @@ tester.run("no-missing-g-flag", rule as any, {
         const s = 'foo'
         const ret = s.replaceAll(new RegExp('foo', unknown), 'bar')
         `,
+        // ES2024
+        String.raw`
+        const s = 'foo'
+        const ret = s.replaceAll(/[\q{foo}]/gv, 'bar')
+        `,
     ],
     invalid: [
         {
@@ -233,5 +238,25 @@ tester.run("no-missing-g-flag", rule as any, {
                 },
             ],
         },
+        {
+            // ES2024
+            code: String.raw`
+            const s = 'foo'
+            const ret = s.replaceAll(/[\q{foo}]/v, 'bar')
+            `,
+            output: String.raw`
+            const s = 'foo'
+            const ret = s.replaceAll(/[\q{foo}]/vg, 'bar')
+            `,
+            options: [{ strictTypes: false }],
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 38,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts
index 41a5245..5066708 100644
--- a/tests/lib/rules/no-obscure-range.ts
+++ b/tests/lib/rules/no-obscure-range.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-obscure-range"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -23,6 +23,7 @@ tester.run("no-obscure-range", rule as any, {
                 },
             },
         },
+        "/[[0-9]--[6-8]]/v",
     ],
     invalid: [
         {
@@ -125,5 +126,16 @@ tester.run("no-obscure-range", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[[ -\/]--+]/v`,
+            errors: [
+                {
+                    message:
+                        "Unexpected obscure character range. The characters of ' -\\/' (U+0020 - U+002f) are not obvious.",
+                    line: 1,
+                    column: 4,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-octal.ts b/tests/lib/rules/no-octal.ts
index 25b39a7..a8b9e94 100644
--- a/tests/lib/rules/no-octal.ts
+++ b/tests/lib/rules/no-octal.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/no-octal"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-octal", rule as any, {
-    valid: ["/\\0/", "/[\\7]/", "/[\\1-\\4]/"],
+    valid: ["/\\0/", "/[\\7]/", "/[\\1-\\4]/", String.raw`/[\q{\0}]/v`],
     invalid: [
         {
             code: "/\\07/",
diff --git a/tests/lib/rules/no-optional-assertion.ts b/tests/lib/rules/no-optional-assertion.ts
index 5a1b6de..f7aad78 100644
--- a/tests/lib/rules/no-optional-assertion.ts
+++ b/tests/lib/rules/no-optional-assertion.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-optional-assertion"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -15,6 +15,7 @@ tester.run("no-optional-assertion", rule as any, {
         String.raw`/(?:a|(?:\b|a)+)?/`,
         String.raw`/fo(?:o\b)/`,
         String.raw`/fo(?:o\b){1}/`,
+        String.raw`/(?:(?=[\q{a}]))/v`,
     ],
     invalid: [
         {
@@ -89,5 +90,16 @@ tester.run("no-optional-assertion", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/(?:(?=[\q{a}]))?/v`,
+            errors: [
+                {
+                    message:
+                        "This assertion effectively optional and does not change the pattern. Either remove the assertion or change the parent quantifier '?'.",
+                    line: 1,
+                    column: 5,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-potentially-useless-backreference.ts b/tests/lib/rules/no-potentially-useless-backreference.ts
index 7d8e016..1ff2631 100644
--- a/tests/lib/rules/no-potentially-useless-backreference.ts
+++ b/tests/lib/rules/no-potentially-useless-backreference.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-potentially-useless-backreference"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -14,6 +14,7 @@ tester.run("no-potentially-useless-backreference", rule as any, {
         String.raw`/(a*)(?:a|\1)/`,
         String.raw`/(a)+\1/`,
         String.raw`/(?=(a))\1/`,
+        String.raw`/([\q{a}])\1/v`,
 
         // done by regexp/no-useless-backreference
         String.raw`/(a+)b|\1/`,
@@ -84,5 +85,16 @@ tester.run("no-potentially-useless-backreference", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/(?:([\q{a}])|b)\1/v`,
+            errors: [
+                {
+                    message:
+                        "Some paths leading to the backreference do not go through the referenced capturing group or the captured text might be reset before reaching the backreference.",
+                    line: 1,
+                    column: 17,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-standalone-backslash.ts b/tests/lib/rules/no-standalone-backslash.ts
index 7bbff68..266ec30 100644
--- a/tests/lib/rules/no-standalone-backslash.ts
+++ b/tests/lib/rules/no-standalone-backslash.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/no-standalone-backslash"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-standalone-backslash", rule as any, {
-    valid: [String.raw`/\cX/`],
+    valid: [String.raw`/\cX/`, String.raw`/[[\cA-\cZ]--\cX]/v`],
     invalid: [
         {
             code: String.raw`/\c/`,
diff --git a/tests/lib/rules/no-super-linear-backtracking.ts b/tests/lib/rules/no-super-linear-backtracking.ts
index 5bdb8e8..f2948bc 100644
--- a/tests/lib/rules/no-super-linear-backtracking.ts
+++ b/tests/lib/rules/no-super-linear-backtracking.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-super-linear-backtracking"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -13,6 +13,7 @@ tester.run("no-super-linear-backtracking", rule as any, {
         String.raw`/regexp/`,
         String.raw`/a+b+a+b+/`,
         String.raw`/\w+\b[\w-]+/`,
+        String.raw`/[\q{ab}]*[\q{ab}]*$/v`, // Limitation of scslre
     ],
     invalid: [
         // self
@@ -53,5 +54,12 @@ tester.run("no-super-linear-backtracking", rule as any, {
                 "The quantifier '\\w+' can exchange characters with '\\w+'. Using any string accepted by /b+/, this can be exploited to cause at least polynomial backtracking. This might cause exponential backtracking.",
             ],
         },
+        {
+            code: String.raw`/[\q{a}]*b?[\q{a}]+$/v`,
+            output: String.raw`/(?:[\q{a}]+(?:b[\q{a}]+)?|b[\q{a}]+)$/v`,
+            errors: [
+                "The quantifier '[\\q{a}]*' can exchange characters with '[\\q{a}]+'. Using any string accepted by /a+/, this can be exploited to cause at least polynomial backtracking. This might cause exponential backtracking.",
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-super-linear-move.ts b/tests/lib/rules/no-super-linear-move.ts
index 0a6cd75..8b4c8d3 100644
--- a/tests/lib/rules/no-super-linear-move.ts
+++ b/tests/lib/rules/no-super-linear-move.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-super-linear-move"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -26,6 +26,7 @@ tester.run("no-super-linear-move", rule as any, {
         },
         { code: String.raw`/a*b/.source`, options: [{ ignorePartial: true }] },
         { code: String.raw`/a*b/y`, options: [{ ignoreSticky: true }] },
+        String.raw`/[\q{abc}]+/v`,
     ],
     invalid: [
         {
@@ -80,5 +81,11 @@ tester.run("no-super-linear-move", rule as any, {
                 "Any attack string /a+/ plus some rejecting suffix will cause quadratic runtime because of this quantifier.",
             ],
         },
+        {
+            code: String.raw`/[\q{abc}]+a/v`,
+            errors: [
+                "Any attack string /(?:abc)+/ plus some rejecting suffix will cause quadratic runtime because of this quantifier.",
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-trivially-nested-assertion.ts b/tests/lib/rules/no-trivially-nested-assertion.ts
index 1dd0a0d..98a9e7c 100644
--- a/tests/lib/rules/no-trivially-nested-assertion.ts
+++ b/tests/lib/rules/no-trivially-nested-assertion.ts
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-trivially-nested-assertion"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -22,6 +22,9 @@ tester.run("no-trivially-nested-assertion", rule as any, {
         // guaranteed to be reset, so we can't transform them into one
         // non-negated lookaround
         `/(?!(?!(a)))/`,
+
+        // ES2024
+        String.raw`/(?=[\q{$}])/v`,
     ],
     invalid: [
         {
diff --git a/tests/lib/rules/no-trivially-nested-quantifier.ts b/tests/lib/rules/no-trivially-nested-quantifier.ts
index 6ea417f..620a77d 100644
--- a/tests/lib/rules/no-trivially-nested-quantifier.ts
+++ b/tests/lib/rules/no-trivially-nested-quantifier.ts
@@ -3,13 +3,19 @@ import rule from "../../../lib/rules/no-trivially-nested-quantifier"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-trivially-nested-quantifier", rule as any, {
-    valid: [`/(a?)+/`, `/(?:a{2})+/`, `/(?:a{3,4})+/`, `/(?:a+?)+/`],
+    valid: [
+        `/(a?)+/`,
+        `/(?:a{2})+/`,
+        `/(?:a{3,4})+/`,
+        `/(?:a+?)+/`,
+        String.raw`/(?:[\q{a}])+/v`,
+    ],
     invalid: [
         {
             code: String.raw`/(?:a?)+/`,
@@ -110,5 +116,12 @@ tester.run("no-trivially-nested-quantifier", rule as any, {
                 { message: "This nested quantifier can be simplified to '?'." },
             ],
         },
+        {
+            code: String.raw`/(?:[\q{a}]+)+/v`,
+            output: String.raw`/[\q{a}]+/v`,
+            errors: [
+                "These two quantifiers are trivially nested and can be replaced with '+'.",
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-unused-capturing-group.ts b/tests/lib/rules/no-unused-capturing-group.ts
index ce7c812..919c45b 100644
--- a/tests/lib/rules/no-unused-capturing-group.ts
+++ b/tests/lib/rules/no-unused-capturing-group.ts
@@ -5,7 +5,7 @@ import rule from "../../../lib/rules/no-unused-capturing-group"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
@@ -148,6 +148,10 @@ tester.run("no-unused-capturing-group", rule as any, {
         const element = [...'abc_abc_abb_acc'.matchAll(/(?<a>.)(?<b>.)(?<c>.)/g)].find(m => m.groups.b === m.groups.c);
         console.log(element.groups.a);
         `,
+        String.raw`
+        const text = 'Lorem ipsum dolor sit amet'
+        const replaced = text.replace(/([\q{Lorem|ipsum}])/vg, '**$1**')
+        `,
     ],
     invalid: [
         {
@@ -530,5 +534,17 @@ tester.run("no-unused-capturing-group", rule as any, {
             options: [{ fixable: true }],
             errors: ["Capturing group 'b' is defined but never used."],
         },
+        {
+            code: String.raw`
+            const text = 'Lorem ipsum dolor sit amet'
+            const replaced = text.replace(/([\q{Lorem|ipsum}])/gv, '**Lorem ipsum**')
+            `,
+            output: String.raw`
+            const text = 'Lorem ipsum dolor sit amet'
+            const replaced = text.replace(/(?:[\q{Lorem|ipsum}])/gv, '**Lorem ipsum**')
+            `,
+            options: [{ fixable: true }],
+            errors: ["Capturing group number 1 is defined but never used."],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-useless-string-literal.ts b/tests/lib/rules/no-useless-string-literal.ts
index 764f270..ac24157 100644
--- a/tests/lib/rules/no-useless-string-literal.ts
+++ b/tests/lib/rules/no-useless-string-literal.ts
@@ -79,6 +79,11 @@ tester.run("no-useless-string-literal", rule as any, {
             output: String.raw`/[A&&\&]/v`,
             errors: ["Unexpected string disjunction of single character."],
         },
+        {
+            code: String.raw`/[\q{&}&&A]/v`,
+            output: String.raw`/[\&&&A]/v`,
+            errors: ["Unexpected string disjunction of single character."],
+        },
         {
             code: String.raw`/[A&&\q{^|ab}]/v`,
             output: String.raw`/[A&&[\^\q{ab}]]/v`,