Repository: ota-meshi/eslint-plugin-regexp
Pull request #331 (2021-09-19 11:06:00)

Add `regexp/require-unicode-regexp` rule
relates to #325.

I also made some changes to the `RegExpContext`.

- `getFlagsLocation`: If a regex literals doesn't have any flags, it will now point to the last `/` instead of the characters after the last `/`.
- `buildRegexpVisitor`: It will now try to dereference owned variables given as the flags argument. This means that we will be able to fix more flag reports.
- `fixReplaceFlags`: If there is no flags argument given to a `RegExp` constructor (and the first argument is a string), then trying to add flags will add a flags string. E.g. `RegExp("foo")` -> `RegExp("foo", "u")`
- I changed the semantics of `RegExpContext.flagsString === null`. It used to mean "no flag argument OR unable to evaluate flag argument". It now only means "unable to evaluate flag argument". This semantic is more useful and necessary for the correct of `require-unicode-regexp`.


Patch:
diff --git a/README.md b/README.md
index 09f06f0..4289bdf 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-dupe-disjunctions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-disjunctions.html) | disallow duplicate disjunctions | :star: |
 | [regexp/no-empty-alternative](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-alternative.html) | disallow alternatives without elements | :star: |
 | [regexp/no-empty-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-capturing-group.html) | disallow capturing group that captures empty. | :star: |
+| [regexp/no-empty-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-character-class.html) | disallow character classes that match no characters |  |
 | [regexp/no-empty-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-group.html) | disallow empty group | :star: |
 | [regexp/no-empty-lookarounds-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-lookarounds-assertion.html) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
 | [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
@@ -154,6 +155,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/prefer-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-range.html) | enforce using character class range | :star::wrench: |
 | [regexp/prefer-regexp-exec](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-exec.html) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided |  |
 | [regexp/prefer-regexp-test](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-test.html) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | :wrench: |
+| [regexp/require-unicode-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html) | enforce the use of the `u` flag | :wrench: |
 | [regexp/sort-alternatives](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-alternatives.html) | sort alternatives if order doesn't matter | :wrench: |
 
 ### Stylistic Issues
@@ -167,7 +169,9 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-useless-non-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-capturing-group.html) | disallow unnecessary Non-capturing group | :star::wrench: |
 | [regexp/prefer-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-character-class.html) | enforce using character class | :star::wrench: |
 | [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |
+| [regexp/prefer-lookaround](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-lookaround.html) | prefer lookarounds over capturing group that do not replace | :wrench: |
 | [regexp/prefer-named-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-backreference.html) | enforce using named backreferences | :wrench: |
+| [regexp/prefer-named-capture-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-capture-group.html) | enforce using named capture groups |  |
 | [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
 | [regexp/prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | :star::wrench: |
 | [regexp/prefer-star-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html) | enforce using `*` quantifier | :star::wrench: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 64d1897..9de4f69 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -17,6 +17,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-dupe-disjunctions](./no-dupe-disjunctions.md) | disallow duplicate disjunctions | :star: |
 | [regexp/no-empty-alternative](./no-empty-alternative.md) | disallow alternatives without elements | :star: |
 | [regexp/no-empty-capturing-group](./no-empty-capturing-group.md) | disallow capturing group that captures empty. | :star: |
+| [regexp/no-empty-character-class](./no-empty-character-class.md) | disallow character classes that match no characters |  |
 | [regexp/no-empty-group](./no-empty-group.md) | disallow empty group | :star: |
 | [regexp/no-empty-lookarounds-assertion](./no-empty-lookarounds-assertion.md) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
 | [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
@@ -63,6 +64,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/prefer-range](./prefer-range.md) | enforce using character class range | :star::wrench: |
 | [regexp/prefer-regexp-exec](./prefer-regexp-exec.md) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided |  |
 | [regexp/prefer-regexp-test](./prefer-regexp-test.md) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | :wrench: |
+| [regexp/require-unicode-regexp](./require-unicode-regexp.md) | enforce the use of the `u` flag | :wrench: |
 | [regexp/sort-alternatives](./sort-alternatives.md) | sort alternatives if order doesn't matter | :wrench: |
 
 ### Stylistic Issues
@@ -76,7 +78,9 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :star::wrench: |
 | [regexp/prefer-character-class](./prefer-character-class.md) | enforce using character class | :star::wrench: |
 | [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
+| [regexp/prefer-lookaround](./prefer-lookaround.md) | prefer lookarounds over capturing group that do not replace | :wrench: |
 | [regexp/prefer-named-backreference](./prefer-named-backreference.md) | enforce using named backreferences | :wrench: |
+| [regexp/prefer-named-capture-group](./prefer-named-capture-group.md) | enforce using named capture groups |  |
 | [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
 | [regexp/prefer-question-quantifier](./prefer-question-quantifier.md) | enforce using `?` quantifier | :star::wrench: |
 | [regexp/prefer-star-quantifier](./prefer-star-quantifier.md) | enforce using `*` quantifier | :star::wrench: |
diff --git a/docs/rules/no-empty-character-class.md b/docs/rules/no-empty-character-class.md
new file mode 100644
index 0000000..417985c
--- /dev/null
+++ b/docs/rules/no-empty-character-class.md
@@ -0,0 +1,52 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-empty-character-class"
+description: "disallow character classes that match no characters"
+---
+# regexp/no-empty-character-class
+
+> disallow character classes that match no characters
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule reports character classes that cannot match any characters.
+
+Character classes that cannot match any characters are either empty or negated character classes of elements that contain all characters.
+
+The reports for this rule include reports for the ESLint core [no-empty-character-class] rule. That is, if you use this rule, you can turn off the ESLint core [no-empty-character-class] rule.
+
+<eslint-code-block>
+
+```js
+/* eslint regexp/no-empty-character-class: "error" */
+
+/* ✓ GOOD */
+var foo = /abc[d]/;
+var foo = /abc[a-z]/;
+var foo = /[^]/;
+var foo = /[\s\S]/;
+
+/* ✗ BAD */
+var foo = /abc[]/;
+var foo = /[^\s\S]/;
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [no-empty-character-class]
+
+[no-empty-character-class]: https://eslint.org/docs/rules/no-empty-character-class
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-empty-character-class.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-empty-character-class.ts)
diff --git a/docs/rules/prefer-lookaround.md b/docs/rules/prefer-lookaround.md
new file mode 100644
index 0000000..304135c
--- /dev/null
+++ b/docs/rules/prefer-lookaround.md
@@ -0,0 +1,48 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/prefer-lookaround"
+description: "prefer lookarounds over capturing group that do not replace"
+---
+# regexp/prefer-lookaround
+
+> prefer lookarounds over capturing group that do not replace
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports string replacement using capturing groups that can be replaced with lookaround assertions.
+
+<eslint-code-block fix>
+
+```js
+/* eslint regexp/prefer-lookaround: "error" */
+
+/* ✓ GOOD */
+var str = 'JavaScript'.replace(/Java(?=Script)/g, 'Type')
+
+/* ✗ BAD */
+var str = 'JavaScript'.replace(/Java(Script)/g, 'Type$1')
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "regexp/prefer-lookaround": ["error", {
+      "strictTypes": true
+  }]
+}
+```
+
+- `strictTypes` ... If `true`, strictly check the type of object to determine if the regex instance was used in `replace()` and `replaceAll()`. Default is `true`.  
+  This option is always on when using TypeScript.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-lookaround.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-lookaround.ts)
diff --git a/docs/rules/prefer-named-capture-group.md b/docs/rules/prefer-named-capture-group.md
new file mode 100644
index 0000000..9d74788
--- /dev/null
+++ b/docs/rules/prefer-named-capture-group.md
@@ -0,0 +1,47 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/prefer-named-capture-group"
+description: "enforce using named capture groups"
+---
+# regexp/prefer-named-capture-group
+
+> enforce using named capture groups
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule reports capturing groups without a name.
+
+This rule is inspired by the [prefer-named-capture-group] rule. The positions of reports are improved over the core rule and arguments of `new RegExp()` are also checked.
+
+<eslint-code-block>
+
+```js
+/* eslint regexp/prefer-named-capture-group: "error" */
+
+/* ✓ GOOD */
+var foo = /(?<foo>ba+r)/;
+var foo = /\b(?:foo)+\b/;
+
+/* ✗ BAD */
+var foo = /\b(foo)+\b/;
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [prefer-named-capture-group]
+
+[prefer-named-capture-group]: https://eslint.org/docs/rules/prefer-named-capture-group
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-named-capture-group.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-named-capture-group.ts)
diff --git a/docs/rules/require-unicode-regexp.md b/docs/rules/require-unicode-regexp.md
new file mode 100644
index 0000000..3242f88
--- /dev/null
+++ b/docs/rules/require-unicode-regexp.md
@@ -0,0 +1,53 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/require-unicode-regexp"
+description: "enforce the use of the `u` flag"
+---
+# regexp/require-unicode-regexp
+
+> enforce the use of the `u` flag
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports regular expressions without the `u` flag.
+
+It will automatically add the `u` flag to regular expression where it is statically guaranteed to be safe to do so. In all other cases, the developer has to check that adding the `u` flag doesn't cause the regex to behave incorrectly.
+
+This rule is inspired by the [require-unicode-regexp] rule. The position of the report is improved over the core rule and arguments of `new RegExp()` are also checked.
+
+<eslint-code-block fix>
+
+```js
+/* eslint regexp/require-unicode-regexp: "error" */
+
+/* ✓ GOOD */
+var foo = /foo/u;
+var foo = /a\s+b/u;
+
+/* ✗ BAD */
+var foo = /foo/;
+var foo = RegExp("a\\s+b");
+var foo = /[a-z]/i;
+var foo = /\S/;
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [require-unicode-regexp]
+
+[require-unicode-regexp]: https://eslint.org/docs/rules/require-unicode-regexp
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/require-unicode-regexp.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/require-unicode-regexp.ts)
diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts
index ab19b6d..ba8c0df 100644
--- a/lib/configs/recommended.ts
+++ b/lib/configs/recommended.ts
@@ -10,6 +10,8 @@ export = {
         // The ESLint rule will report fewer cases than our rule
         "no-invalid-regexp": "off",
         "no-useless-backreference": "off",
+        // TODO Switch in the major version.
+        // "no-empty-character-class": "off",
 
         // eslint-plugin-regexp rules
         "regexp/confusing-quantifier": "warn",
diff --git a/lib/rules/no-empty-character-class.ts b/lib/rules/no-empty-character-class.ts
new file mode 100644
index 0000000..d4216a8
--- /dev/null
+++ b/lib/rules/no-empty-character-class.ts
@@ -0,0 +1,51 @@
+import { matchesNoCharacters } from "regexp-ast-analysis"
+import type { RegExpVisitor } from "regexpp/visitor"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+
+export default createRule("no-empty-character-class", {
+    meta: {
+        docs: {
+            description: "disallow character classes that match no characters",
+            category: "Possible Errors",
+            // TODO Switch to recommended in the major version.
+            // recommended: true,
+            recommended: false,
+        },
+        schema: [],
+        messages: {
+            empty:
+                "This character class matches no characters because it is empty.",
+            cannotMatchAny: "This character class cannot match any characters.",
+        },
+        type: "suggestion", // "problem",
+    },
+    create(context) {
+        /**
+         * Create visitor
+         */
+        function createVisitor(
+            regexpContext: RegExpContext,
+        ): RegExpVisitor.Handlers {
+            const { node, getRegexpLocation, flags } = regexpContext
+
+            return {
+                onCharacterClassEnter(ccNode) {
+                    if (matchesNoCharacters(ccNode, flags)) {
+                        context.report({
+                            node,
+                            loc: getRegexpLocation(ccNode),
+                            messageId: ccNode.elements.length
+                                ? "cannotMatchAny"
+                                : "empty",
+                        })
+                    }
+                },
+            }
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
+    },
+})
diff --git a/lib/rules/prefer-lookaround.ts b/lib/rules/prefer-lookaround.ts
new file mode 100644
index 0000000..3c9f762
--- /dev/null
+++ b/lib/rules/prefer-lookaround.ts
@@ -0,0 +1,675 @@
+import type { RegExpVisitor } from "regexpp/visitor"
+import type { CapturingGroup, Element } from "regexpp/ast"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import { createTypeTracker } from "../utils/type-tracker"
+import type { KnownMethodCall, ReferenceElement } from "../utils/ast-utils"
+import {
+    getParent,
+    parseReplacements,
+    getStaticValue,
+    extractExpressionReferences,
+    isKnownMethodCall,
+} from "../utils/ast-utils"
+import type { PatternReplaceRange } from "../utils/ast-utils/pattern-source"
+import type { Expression, Literal } from "estree"
+import type { Rule } from "eslint"
+import { mention } from "../utils/mention"
+import {
+    getFirstConsumedCharPlusAfter,
+    getPossiblyConsumedChar,
+} from "../utils/regexp-ast"
+import {
+    getLengthRange,
+    isZeroLength,
+    FirstConsumedChars,
+} from "regexp-ast-analysis"
+import type { CharSet } from "refa"
+
+type ReplaceReference = { ref: string | number; range?: [number, number] }
+type ReplaceReferences = {
+    // Reference at the starting position.
+    // e.g.
+    // '$1str$2' -> { ref: 1, range: [0,2] }
+    // '$<foo>str$<bar>' -> { ref: 'foo', range: [0,6] }
+    // 'str$1' -> null
+    // 'str$1str$2' -> null
+    startRef: ReplaceReference | null
+    // Reference at the ending position.
+    // e.g.
+    // '$1str$2' -> { ref: 2, range: [5,7] }
+    // '$<foo>str$<bar>' -> { ref: 'bar', range: [9,15] }
+    // '$1str' -> null
+    // '$1str$2str' -> null
+    endRef: ReplaceReference | null
+    // All references including the above.
+    allRefs: ReplaceReference[]
+}
+
+/**
+ * Holds all replacement reference data.
+ *
+ * If the same RegExp instance is used for replacement in 2 places, the number of data in `list` is 2.
+ */
+class ReplaceReferencesList {
+    private readonly list: ReplaceReferences[]
+
+    /** Reference name at the starting position. */
+    public readonly startRefName?: string | number
+
+    /** Reference name at the ending position. */
+    public readonly endRefName?: string | number
+
+    /** All reference names except at the starting position. */
+    public readonly otherThanStartRefNames: Set<string | number>
+
+    /** All reference names except at the starting position. */
+    public readonly otherThanEndRefNames: Set<string | number>
+
+    public constructor(list: ReplaceReferences[]) {
+        this.list = list
+        this.startRefName = list[0].startRef?.ref
+        this.endRefName = list[0].endRef?.ref
+
+        const otherThanStartRefNames = new Set<string | number>()
+        const otherThanEndRefNames = new Set<string | number>()
+        for (const { startRef, endRef, allRefs } of this.list) {
+            for (const ref of allRefs) {
+                if (ref !== startRef) {
+                    otherThanStartRefNames.add(ref.ref)
+                }
+                if (ref !== endRef) {
+                    otherThanEndRefNames.add(ref.ref)
+                }
+            }
+        }
+        this.otherThanStartRefNames = otherThanStartRefNames
+        this.otherThanEndRefNames = otherThanEndRefNames
+    }
+
+    public *[Symbol.iterator](): Iterator<ReplaceReferences> {
+        yield* this.list
+    }
+}
+
+const enum SideEffect {
+    startRef,
+    endRef,
+}
+
+/**
+ * Gets the type of side effect when replacing the capture group for the given element.
+ *
+ * There are no side effects if the following conditions are met:
+ *
+ * - Some elements other than the start capturing group have disjoints to the start capturing group.
+ * - The last element and the start consume character have disjoint.
+ */
+function getSideEffectsWhenReplacingCapturingGroup(
+    elements: readonly Element[],
+    start: CapturingGroup | undefined,
+    end: CapturingGroup | undefined,
+    { flags }: RegExpContext,
+): Set<SideEffect> {
+    const result = new Set<SideEffect>()
+
+    if (start) {
+        const { char } = getPossiblyConsumedChar(start, flags)
+        if (!hasDisjoint(char, elements.slice(1))) {
+            result.add(SideEffect.startRef)
+        } else {
+            const last = elements[elements.length - 1]
+            const lastChar = FirstConsumedChars.toLook(
+                getFirstConsumedCharPlusAfter(last, "rtl", flags),
+            )
+            if (!lastChar.char.isDisjointWith(char)) {
+                result.add(SideEffect.startRef)
+            }
+        }
+    }
+
+    if (end && flags.global) {
+        const first = elements[0]
+        if (first) {
+            const { char } = getPossiblyConsumedChar(end, flags)
+
+            const firstChar = FirstConsumedChars.toLook(
+                getFirstConsumedCharPlusAfter(first, "ltr", flags),
+            )
+            if (!firstChar.char.isDisjointWith(char)) {
+                result.add(SideEffect.endRef)
+            }
+        }
+    }
+
+    return result
+
+    /** Checks whether the given target element has disjoint in elements.  */
+    function hasDisjoint(target: CharSet, targetElements: Element[]) {
+        for (const element of targetElements) {
+            if (isConstantLength(element)) {
+                const elementChars = getPossiblyConsumedChar(element, flags)
+                if (elementChars.char.isEmpty) {
+                    continue
+                }
+                if (elementChars.char.isDisjointWith(target)) {
+                    return true
+                }
+            } else {
+                const elementLook = FirstConsumedChars.toLook(
+                    getFirstConsumedCharPlusAfter(element, "ltr", flags),
+                )
+                return elementLook.char.isDisjointWith(target)
+            }
+        }
+        return false
+    }
+
+    /** Checks whether the given element is constant length. */
+    function isConstantLength(target: Element): boolean {
+        const range = getLengthRange(target)
+        return Boolean(range && range.min === range.max)
+    }
+}
+
+/**
+ * Parse option
+ */
+function parseOption(
+    userOption:
+        | {
+              strictTypes?: boolean
+          }
+        | undefined,
+) {
+    let strictTypes = true
+    if (userOption) {
+        if (userOption.strictTypes != null) {
+            strictTypes = userOption.strictTypes
+        }
+    }
+
+    return {
+        strictTypes,
+    }
+}
+
+export default createRule("prefer-lookaround", {
+    meta: {
+        docs: {
+            description:
+                "prefer lookarounds over capturing group that do not replace",
+            category: "Stylistic Issues",
+            recommended: false,
+        },
+        fixable: "code",
+        schema: [
+            {
+                type: "object",
+                properties: {
+                    strictTypes: { type: "boolean" },
+                },
+                additionalProperties: false,
+            },
+        ],
+        messages: {
+            preferLookarounds:
+                "These capturing groups can be replaced with lookaround assertions ({{expr1}} and {{expr2}}).",
+            prefer:
+                "This capturing group can be replaced with a {{kind}} ({{expr}}).",
+        },
+        type: "suggestion",
+    },
+    create(context) {
+        const { strictTypes } = parseOption(context.options[0])
+        const typeTracer = createTypeTracker(context)
+
+        /**
+         * Create visitor
+         */
+        function createVisitor(
+            regexpContext: RegExpContext,
+        ): RegExpVisitor.Handlers {
+            const { regexpNode, patternAst } = regexpContext
+            if (
+                patternAst.alternatives.length > 1 ||
+                patternAst.alternatives[0].elements.length < 2
+            ) {
+                return {}
+            }
+            const replaceReferenceList: ReplaceReferences[] = []
+            for (const ref of extractExpressionReferences(
+                regexpNode,
+                context,
+            )) {
+                if (ref.type === "argument") {
+                    if (
+                        !isKnownMethodCall(ref.callExpression, {
+                            replace: 2,
+                            replaceAll: 2,
+                        })
+                    ) {
+                        // Calls other than replace.
+                        return {}
+                    }
+                    const replaceReference = getReplaceReferenceFromCallExpression(
+                        ref.callExpression,
+                    )
+                    if (!replaceReference) {
+                        // Unknown call or replacement where lookarounds cannot be used.
+                        return {}
+                    }
+                    replaceReferenceList.push(replaceReference)
+                } else if (ref.type === "member") {
+                    const parent = getParent(ref.memberExpression)
+                    if (
+                        parent?.type === "CallExpression" &&
+                        isKnownMethodCall(parent, {
+                            test: 1,
+                        }) &&
+                        !regexpContext.flags.global
+                    ) {
+                        // Using it in the `test` method has no effect on rewriting the regex.
+                        continue
+                    }
+                    // Cannot trace.
+                    return {}
+                } else {
+                    // Cannot trace.
+                    return {}
+                }
+            }
+            if (!replaceReferenceList.length) {
+                // No pattern is used.
+                return {}
+            }
+            const replaceReference = replaceReferenceList[0]
+            if (
+                replaceReferenceList.some(
+                    (target) =>
+                        target.startRef?.ref !==
+                            replaceReference.startRef?.ref ||
+                        target.endRef?.ref !== replaceReference.endRef?.ref,
+                )
+            ) {
+                // Lookaround cannot be used as it is used in various replacements.
+                return {}
+            }
+            return createVerifyVisitor(
+                regexpContext,
+                new ReplaceReferencesList(replaceReferenceList),
+            )
+        }
+
+        /**
+         * Get the replace reference info from given call expression
+         */
+        function getReplaceReferenceFromCallExpression(
+            node: KnownMethodCall,
+        ): ReplaceReferences | null {
+            if (
+                strictTypes
+                    ? !typeTracer.isString(node.callee.object)
+                    : !typeTracer.maybeString(node.callee.object)
+            ) {
+                // The callee object is not a string.
+                return null
+            }
+
+            const replacementNode = node.arguments[1]
+            if (replacementNode.type === "Literal") {
+                return getReplaceReferenceFromLiteralReplacementArgument(
+                    replacementNode,
+                )
+            }
+            return getReplaceReferenceFromNonLiteralReplacementArgument(
+                replacementNode,
+            )
+        }
+
+        /**
+         * Get the replace reference info from given literal replacement argument
+         */
+        function getReplaceReferenceFromLiteralReplacementArgument(
+            node: Literal,
+        ): ReplaceReferences | null {
+            if (typeof node.value !== "string") {
+                return null
+            }
+            const replacements = parseReplacements(context, node)
+
+            let startRef: ReplaceReference | null = null
+            let endRef: ReplaceReference | null = null
+            const start = replacements[0]
+            if (start?.type === "ReferenceElement") {
+                startRef = start
+            }
+            const end = replacements[replacements.length - 1]
+            if (end?.type === "ReferenceElement") {
+                endRef = end
+            }
+            if (!startRef && !endRef) {
+                // Not using a capturing group at start or end.
+                return null
+            }
+            return {
+                startRef,
+                endRef,
+                allRefs: replacements.filter(
+                    (e): e is ReferenceElement => e.type === "ReferenceElement",
+                ),
+            }
+        }
+
+        /**
+         * Get the replace reference info from given non-literal replacement argument
+         */
+        function getReplaceReferenceFromNonLiteralReplacementArgument(
+            node: Expression,
+        ): ReplaceReferences | null {
+            const evaluated = getStaticValue(context, node)
+            if (!evaluated || typeof evaluated.value !== "string") {
+                // The replacement string cannot be determined.
+                return null
+            }
+            const refRegex = /\$([1-9]\d*|<([^>]+)>)/gu
+
+            const allRefs: ReplaceReference[] = []
+            let startRef: ReplaceReference | null = null
+            let endRef: ReplaceReference | null = null
+            let re
+            while ((re = refRegex.exec(evaluated.value))) {
+                const ref = { ref: re[2] ? re[2] : Number(re[1]) }
+                if (re.index === 0) {
+                    startRef = ref
+                }
+                if (refRegex.lastIndex === evaluated.value.length) {
+                    endRef = ref
+                }
+                allRefs.push(ref)
+            }
+            if (!startRef && !endRef) {
+                // Not using a capturing group at start or end.
+                return null
+            }
+            return {
+                startRef,
+                endRef,
+                allRefs,
+            }
+        }
+
+        /**
+         * Create visitor for verify capturing groups
+         */
+        function createVerifyVisitor(
+            regexpContext: RegExpContext,
+            replaceReferenceList: ReplaceReferencesList,
+        ): RegExpVisitor.Handlers {
+            type RefState = {
+                capturingGroups: CapturingGroup[]
+                isUseOther?: boolean
+                capturingNum: number
+            }
+            const startRefState: RefState = {
+                capturingGroups: [],
+                capturingNum: -1,
+            }
+            const endRefState: RefState = {
+                capturingGroups: [],
+                capturingNum: -1,
+            }
+
+            let refNum = 0
+            return {
+                onCapturingGroupEnter(cgNode) {
+                    refNum++
+                    processForState(
+                        replaceReferenceList.startRefName,
+                        replaceReferenceList.otherThanStartRefNames,
+                        startRefState,
+                    )
+                    processForState(
+                        replaceReferenceList.endRefName,
+                        replaceReferenceList.otherThanEndRefNames,
+                        endRefState,
+                    )
+
+                    /** Process state */
+                    function processForState(
+                        refName: string | number | undefined,
+                        otherThanRefNames: Set<string | number>,
+                        state: RefState,
+                    ) {
+                        if (refName === refNum || refName === cgNode.name) {
+                            state.capturingGroups.push(cgNode)
+                            state.capturingNum = refNum
+                            // Flags the capturing group referenced in `refName` if it is also referenced elsewhere.
+                            state.isUseOther ||= Boolean(
+                                otherThanRefNames.has(refNum) ||
+                                    (cgNode.name &&
+                                        otherThanRefNames.has(cgNode.name)),
+                            )
+                        }
+                    }
+                },
+                onPatternLeave(pNode) {
+                    // verify
+                    const alt = pNode.alternatives[0]
+                    let reportStart = null
+                    if (
+                        !startRefState.isUseOther &&
+                        startRefState.capturingGroups.length === 1 && // It will not be referenced from more than one, but check it just in case.
+                        startRefState.capturingGroups[0] === alt.elements[0] &&
+                        !isZeroLength(startRefState.capturingGroups[0])
+                    ) {
+                        const capturingGroup = startRefState.capturingGroups[0]
+                        reportStart = {
+                            capturingGroup,
+                            expr: `(?<=${capturingGroup.alternatives
+                                .map((a) => a.raw)
+                                .join("|")})`,
+                        }
+                    }
+                    let reportEnd = null
+                    if (
+                        !endRefState.isUseOther &&
+                        endRefState.capturingGroups.length === 1 && // It will not be referenced from more than one, but check it just in case.
+                        endRefState.capturingGroups[0] ===
+                            alt.elements[alt.elements.length - 1] &&
+                        !isZeroLength(endRefState.capturingGroups[0])
+                    ) {
+                        const capturingGroup = endRefState.capturingGroups[0]
+                        reportEnd = {
+                            capturingGroup,
+                            expr: `(?=${capturingGroup.alternatives
+                                .map((a) => a.raw)
+                                .join("|")})`,
+                        }
+                    }
+                    const sideEffects = getSideEffectsWhenReplacingCapturingGroup(
+                        alt.elements,
+                        reportStart?.capturingGroup,
+                        reportEnd?.capturingGroup,
+                        regexpContext,
+                    )
+                    if (sideEffects.has(SideEffect.startRef)) {
+                        reportStart = null
+                    }
+                    if (sideEffects.has(SideEffect.endRef)) {
+                        reportEnd = null
+                    }
+
+                    if (reportStart && reportEnd) {
+                        const fix = buildFixer(
+                            regexpContext,
+                            [reportStart, reportEnd],
+                            replaceReferenceList,
+                            (target) => {
+                                if (
+                                    target.allRefs.some(
+                                        (ref) =>
+                                            ref !== target.startRef &&
+                                            ref !== target.endRef,
+                                    )
+                                ) {
+                                    // If the capturing group is used for something other than the replacement refs, it cannot be fixed.
+                                    return null
+                                }
+                                return [
+                                    target.startRef?.range,
+                                    target.endRef?.range,
+                                ]
+                            },
+                        )
+                        for (const report of [reportStart, reportEnd]) {
+                            context.report({
+                                loc: regexpContext.getRegexpLocation(
+                                    report.capturingGroup,
+                                ),
+                                messageId: "preferLookarounds",
+                                data: {
+                                    expr1: mention(reportStart.expr),
+                                    expr2: mention(reportEnd.expr),
+                                },
+                                fix,
+                            })
+                        }
+                    } else if (reportStart) {
+                        const fix = buildFixer(
+                            regexpContext,
+                            [reportStart],
+                            replaceReferenceList,
+                            (target) => {
+                                if (
+                                    target.allRefs.some(
+                                        (ref) => ref !== target.startRef,
+                                    )
+                                ) {
+                                    // If the capturing group is used for something other than the replacement refs, it cannot be fixed.
+                                    return null
+                                }
+                                return [target.startRef?.range]
+                            },
+                        )
+                        context.report({
+                            loc: regexpContext.getRegexpLocation(
+                                reportStart.capturingGroup,
+                            ),
+                            messageId: "prefer",
+                            data: {
+                                kind: "lookbehind assertion",
+                                expr: mention(reportStart.expr),
+                            },
+                            fix,
+                        })
+                    } else if (reportEnd) {
+                        const fix = buildFixer(
+                            regexpContext,
+                            [reportEnd],
+                            replaceReferenceList,
+                            (target) => {
+                                if (
+                                    target.allRefs.some((ref) => {
+                                        if (
+                                            ref === target.endRef ||
+                                            typeof ref.ref !== "number"
+                                        ) {
+                                            return false
+                                        }
+                                        return (
+                                            endRefState.capturingNum <= ref.ref
+                                        )
+                                    })
+                                ) {
+                                    // If the capturing group with a large num is used, it cannot be fixed.
+                                    return null
+                                }
+                                return [target.endRef?.range]
+                            },
+                        )
+                        context.report({
+                            loc: regexpContext.getRegexpLocation(
+                                reportEnd.capturingGroup,
+                            ),
+                            messageId: "prefer",
+                            data: {
+                                kind: "lookahead assertion",
+                                expr: mention(reportEnd.expr),
+                            },
+                            fix,
+                        })
+                    }
+                },
+            }
+        }
+
+        /**
+         * Build fixer function
+         */
+        function buildFixer(
+            regexpContext: RegExpContext,
+            replaceCapturingGroups: {
+                capturingGroup: CapturingGroup
+                expr: string
+            }[],
+            replaceReferenceList: ReplaceReferencesList,
+            getRemoveRanges: (
+                replaceReference: ReplaceReferences,
+            ) => Iterable<[number, number] | undefined> | null,
+        ): ((fixer: Rule.RuleFixer) => Rule.Fix[]) | null {
+            const removeRanges: [number, number][] = []
+            for (const replaceReference of replaceReferenceList) {
+                const targetRemoveRanges = getRemoveRanges(replaceReference)
+                if (!targetRemoveRanges) {
+                    return null
+                }
+                for (const range of targetRemoveRanges) {
+                    if (!range) {
+                        return null
+                    }
+                    removeRanges.push(range)
+                }
+            }
+            const replaces: {
+                replaceRange: PatternReplaceRange
+                expr: string
+            }[] = []
+            for (const { capturingGroup, expr } of replaceCapturingGroups) {
+                const replaceRange = regexpContext.patternSource.getReplaceRange(
+                    capturingGroup,
+                )
+                if (!replaceRange) {
+                    return null
+                }
+                replaces.push({
+                    replaceRange,
+                    expr,
+                })
+            }
+
+            return (fixer) => {
+                const list: { offset: number; fix: () => Rule.Fix }[] = []
+                for (const removeRange of removeRanges) {
+                    list.push({
+                        offset: removeRange[0],
+                        fix: () => fixer.removeRange(removeRange),
+                    })
+                }
+                for (const { replaceRange, expr } of replaces) {
+                    list.push({
+                        offset: replaceRange.range[0],
+                        fix: () => replaceRange.replace(fixer, expr),
+                    })
+                }
+                return list
+                    .sort((a, b) => a.offset - b.offset)
+                    .map((item) => item.fix())
+            }
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
+    },
+})
diff --git a/lib/rules/prefer-named-capture-group.ts b/lib/rules/prefer-named-capture-group.ts
new file mode 100644
index 0000000..87ff975
--- /dev/null
+++ b/lib/rules/prefer-named-capture-group.ts
@@ -0,0 +1,49 @@
+import type { RegExpVisitor } from "regexpp/visitor"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import { mention } from "../utils/mention"
+
+export default createRule("prefer-named-capture-group", {
+    meta: {
+        docs: {
+            description: "enforce using named capture groups",
+            category: "Stylistic Issues",
+            recommended: false,
+        },
+        schema: [],
+        messages: {
+            required:
+                "Capture group {{group}} should be converted to a named or non-capturing group.",
+        },
+        type: "suggestion",
+    },
+    create(context) {
+        /**
+         * Create visitor
+         */
+        function createVisitor(
+            regexpContext: RegExpContext,
+        ): RegExpVisitor.Handlers {
+            const { node, getRegexpLocation } = regexpContext
+
+            return {
+                onCapturingGroupEnter(cgNode) {
+                    if (cgNode.name === null) {
+                        context.report({
+                            node,
+                            loc: getRegexpLocation(cgNode),
+                            messageId: "required",
+                            data: {
+                                group: mention(cgNode),
+                            },
+                        })
+                    }
+                },
+            }
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
+    },
+})
diff --git a/lib/rules/require-unicode-regexp.ts b/lib/rules/require-unicode-regexp.ts
new file mode 100644
index 0000000..950d955
--- /dev/null
+++ b/lib/rules/require-unicode-regexp.ts
@@ -0,0 +1,252 @@
+import type { CharRange } from "refa"
+import { visitRegExpAST, RegExpParser } from "regexpp"
+import type { Pattern } from "regexpp/ast"
+import type { RegExpVisitor } from "regexpp/visitor"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import { hasSomeDescendant, toCache, toCharSet } from "regexp-ast-analysis"
+
+const UTF16_MAX = 0xffff
+
+/**
+ * Returns whether the given pattern is compatible with unicode-mode on a
+ * syntactical level. So means that:
+ *
+ * 1. The raw regex is syntactically valid with the u flag.
+ * 2. The regex is parsed the same way (*).
+ *
+ * (*) Unicode mode parses surrogates as one character while non-Unicode mode
+ * parses the pair as two separate code points. We will ignore this difference.
+ * We will also ignore the sematic differences between escape sequences and
+ * so on.
+ *
+ * @returns `false` or the parsed Unicode pattern
+ */
+function isSyntacticallyCompatible(pattern: Pattern): false | Pattern {
+    const INCOMPATIBLE = {}
+
+    // See whether it's syntactically valid
+
+    let uPattern
+    try {
+        uPattern = new RegExpParser().parsePattern(
+            pattern.raw,
+            undefined,
+            undefined,
+            true,
+        )
+    } catch (error) {
+        return false
+    }
+
+    // See whether it's parsed the same way
+
+    // We will try to find constructs in the non-Unicode regex that we know
+    // will either result in a syntax error or a different construct. Since
+    // we already checked for syntax errors, we know that it's the second
+    // option.
+
+    // There is another construct that get interpreted differently: Surrogates.
+    // We want to make sure that no surrogate is a quantified element or
+    // character class element.
+
+    try {
+        visitRegExpAST(pattern, {
+            onCharacterEnter(node) {
+                if (/^\\(?![bfnrtv])[A-Za-z]$/.test(node.raw)) {
+                    // All cool Unicode feature are behind escapes like \p.
+                    throw INCOMPATIBLE
+                }
+            },
+        })
+
+        // See no-misleading-character-class for more details
+        visitRegExpAST(uPattern, {
+            onCharacterEnter(node) {
+                if (
+                    node.value > UTF16_MAX &&
+                    (node.parent.type === "CharacterClass" ||
+                        node.parent.type === "CharacterClassRange")
+                ) {
+                    // /[😃]/ != /[😃]/u
+                    throw INCOMPATIBLE
+                }
+            },
+            onQuantifierEnter(node) {
+                if (
+                    node.element.type === "Character" &&
+                    node.element.value > UTF16_MAX
+                ) {
+                    // /😃+/ != /😃+/u
+                    throw INCOMPATIBLE
+                }
+            },
+        })
+    } catch (error) {
+        if (error === INCOMPATIBLE) {
+            return false
+        }
+        // just rethrow
+        throw error
+    }
+
+    return uPattern
+}
+
+const SURROGATES: CharRange = { min: 0xd800, max: 0xdfff }
+
+/** Returns whether the two given ranges are equal. */
+function rangeEqual(a: readonly CharRange[], b: readonly CharRange[]): boolean {
+    if (a.length !== b.length) {
+        return false
+    }
+    for (let i = 0; i < a.length; i++) {
+        const x = a[i]
+        const y = b[i]
+        if (x.min !== y.min || x.max !== y.max) {
+            return false
+        }
+    }
+    return true
+}
+
+/**
+ * Returns whether the regex would keep its behaviour if the u flag were to be
+ * added.
+ */
+function isSemanticallyCompatible(
+    regexpContext: RegExpContext,
+    uPattern: Pattern,
+): boolean {
+    const surrogatePositions = new Set<number>()
+    visitRegExpAST(uPattern, {
+        onCharacterEnter(node) {
+            if (node.value > UTF16_MAX) {
+                for (let i = node.start; i < node.end; i++) {
+                    surrogatePositions.add(i)
+                }
+            }
+        },
+    })
+
+    const pattern = regexpContext.patternAst
+    const flags = regexpContext.flags
+    const uFlags = toCache({ ...flags, unicode: true })
+
+    return !hasSomeDescendant(
+        pattern,
+        (n) => {
+            // The goal is find something that is will change when adding the
+            // Unicode flag.
+
+            // Surrogates don't change
+            if (n.type === "Character" && surrogatePositions.has(n.start)) {
+                return false
+            }
+
+            if (
+                n.type === "Assertion" &&
+                n.kind === "word" &&
+                flags.ignoreCase
+            ) {
+                // The case canonicalization in Unicode mode is different which
+                // causes `\b` and `\B` to accept/reject a few more characters.
+                return true
+            }
+
+            if (
+                n.type === "Character" ||
+                n.type === "CharacterClass" ||
+                n.type === "CharacterSet"
+            ) {
+                const cs = toCharSet(n, flags)
+                if (!cs.isDisjointWith(SURROGATES)) {
+                    // If the character (class/set) contains high or low
+                    // surrogates, then we won't be able to guarantee that the
+                    // Unicode pattern will behave the same way.
+                    return true
+                }
+
+                // Compare the ranges.
+                return !rangeEqual(cs.ranges, toCharSet(n, uFlags).ranges)
+            }
+
+            return false
+        },
+        (n) => {
+            // Don't go into character classes, we already checked them
+            return n.type !== "CharacterClass"
+        },
+    )
+}
+
+/**
+ * Returns whether the regex would keep its behaviour if the u flag were to be
+ * added.
+ */
+function isCompatible(regexpContext: RegExpContext): boolean {
+    const uPattern = isSyntacticallyCompatible(regexpContext.patternAst)
+    if (!uPattern) {
+        return false
+    }
+
+    return isSemanticallyCompatible(regexpContext, uPattern)
+}
+
+export default createRule("require-unicode-regexp", {
+    meta: {
+        docs: {
+            description: "enforce the use of the `u` flag",
+            category: "Best Practices",
+            recommended: false,
+        },
+        schema: [],
+        fixable: "code",
+        messages: {
+            require: "Use the 'u' flag.",
+        },
+        type: "suggestion", // "problem",
+    },
+    create(context) {
+        /**
+         * Create visitor
+         */
+        function createVisitor(
+            regexpContext: RegExpContext,
+        ): RegExpVisitor.Handlers {
+            const {
+                node,
+                flags,
+                flagsString,
+                getFlagsLocation,
+                fixReplaceFlags,
+            } = regexpContext
+
+            if (flagsString === null) {
+                // This means that there are flags (probably) but we were
+                // unable to evaluate them.
+                return {}
+            }
+
+            if (!flags.unicode) {
+                context.report({
+                    node,
+                    loc: getFlagsLocation(),
+                    messageId: "require",
+                    fix: fixReplaceFlags(() => {
+                        if (!isCompatible(regexpContext)) {
+                            return null
+                        }
+                        return `${flagsString}u`
+                    }),
+                })
+            }
+
+            return {}
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
+    },
+})
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 08a75ef..ab4b229 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -13,6 +13,7 @@ import { JS } from "refa"
 import type { UsageOfPattern } from "./get-usage-of-pattern"
 import { getUsageOfPattern } from "./get-usage-of-pattern"
 import {
+    dereferenceOwnedVariable,
     getStringValueRange,
     isRegexpLiteral,
     isStringLiteral,
@@ -407,14 +408,19 @@ function buildRegexpVisitor(
                 let ownsFlags = false
                 if (flagsArg) {
                     if (flagsArg.type !== "SpreadElement") {
-                        flagsNode = flagsArg
-                        flagsString = getStringIfConstant(context, flagsArg)
-                        ownsFlags = isStringLiteral(flagsArg)
+                        flagsNode = dereferenceOwnedVariable(context, flagsArg)
+                        flagsString = getStringIfConstant(context, flagsNode)
+                        ownsFlags = isStringLiteral(flagsNode)
+                    }
+                } else {
+                    if (patternSource && patternSource.regexpValue) {
+                        flagsString = patternSource.regexpValue.flags
+                        ownsFlags = Boolean(patternSource.regexpValue.ownedNode)
+                        flagsNode = patternSource.regexpValue.ownedNode
+                    } else {
+                        flagsString = ""
+                        ownsFlags = true
                     }
-                } else if (patternSource && patternSource.regexpValue) {
-                    flagsString = patternSource.regexpValue.flags
-                    ownsFlags = Boolean(patternSource.regexpValue.ownedNode)
-                    flagsNode = patternSource.regexpValue.ownedNode
                 }
 
                 regexpDataList.push({
@@ -715,6 +721,11 @@ export function getFlagsLocation(
     if (range == null) {
         return flagsNode?.loc ?? regexpNode.loc!
     }
+
+    if (range[0] === range[1]) {
+        range[0]--
+    }
+
     return {
         start: sourceCode.getLocFromIndex(range[0]),
         end: sourceCode.getLocFromIndex(range[1]),
@@ -885,9 +896,24 @@ function fixReplaceFlags(
             )
         }
 
-        const range = getFlagsRange(flagsNode)
-        if (range == null) {
-            return null
+        // eslint-disable-next-line one-var -- x
+        let flagsFix
+        if (flagsNode) {
+            const range = getFlagsRange(flagsNode)
+            if (range == null) {
+                return null
+            }
+            flagsFix = fixer.replaceTextRange(range, newFlags)
+        } else {
+            // If the RegExp call doesn't have a flags argument, we'll add it
+            if (regexpNode.arguments.length !== 1) {
+                return null
+            }
+            const end = regexpNode.range![1]
+            flagsFix = fixer.replaceTextRange(
+                [end - 1, end],
+                `, "${newFlags}")`,
+            )
         }
 
         // fixes that change the pattern generally assume that flags don't
@@ -901,10 +927,7 @@ function fixReplaceFlags(
             return null
         }
 
-        return [
-            patternRange.replace(fixer, patternSource.value),
-            fixer.replaceTextRange(range, newFlags),
-        ]
+        return [patternRange.replace(fixer, patternSource.value), flagsFix]
     }
 }
 
diff --git a/lib/utils/regexp-ast/alternative-prefix.ts b/lib/utils/regexp-ast/alternative-prefix.ts
index 053dbea..0a0b28d 100644
--- a/lib/utils/regexp-ast/alternative-prefix.ts
+++ b/lib/utils/regexp-ast/alternative-prefix.ts
@@ -1,13 +1,8 @@
 import type { CharSet } from "refa"
-import type {
-    FirstConsumedChar,
-    MatchingDirection,
-    ReadonlyFlags,
-} from "regexp-ast-analysis"
+import type { MatchingDirection, ReadonlyFlags } from "regexp-ast-analysis"
 import {
     getFirstCharAfter,
     toCharSet,
-    getFirstConsumedChar,
     getFirstConsumedCharAfter,
     FirstConsumedChars,
     isZeroLength,
@@ -21,6 +16,7 @@ import type {
     Group,
     Quantifier,
 } from "regexpp/ast"
+import { getFirstConsumedCharPlusAfter } from "./common"
 
 const ltrCache = new WeakMap<Alternative, readonly CharSet[]>()
 const rtlCache = new WeakMap<Alternative, readonly CharSet[]>()
@@ -239,30 +235,3 @@ function getQuantifierPrefix(
     chars.push(look.char)
     return { chars, complete: false }
 }
-
-/**
- * This operations is equal to:
- *
- * ```
- * concat(
- *     getFirstConsumedChar(element, direction, flags),
- *     getFirstConsumedCharAfter(element, direction, flags),
- * )
- * ```
- */
-function getFirstConsumedCharPlusAfter(
-    element: Element | Alternative,
-    direction: MatchingDirection,
-    flags: ReadonlyFlags,
-): FirstConsumedChar {
-    const consumed = getFirstConsumedChar(element, direction, flags)
-
-    if (!consumed.empty) {
-        return consumed
-    }
-
-    return FirstConsumedChars.concat(
-        [consumed, getFirstConsumedCharAfter(element, direction, flags)],
-        flags,
-    )
-}
diff --git a/lib/utils/regexp-ast/common.ts b/lib/utils/regexp-ast/common.ts
index 4585ee9..2598c66 100644
--- a/lib/utils/regexp-ast/common.ts
+++ b/lib/utils/regexp-ast/common.ts
@@ -1,2 +1,39 @@
-import type { Node } from "regexpp/ast"
+import type {
+    FirstConsumedChar,
+    MatchingDirection,
+    ReadonlyFlags,
+} from "regexp-ast-analysis"
+import {
+    FirstConsumedChars,
+    getFirstConsumedChar,
+    getFirstConsumedCharAfter,
+} from "regexp-ast-analysis"
+import type { Alternative, Element, Node } from "regexpp/ast"
 export type ShortCircuit = (aNode: Node, bNode: Node) => boolean | null
+
+/**
+ * This operations is equal to:
+ *
+ * ```
+ * concat(
+ *     getFirstConsumedChar(element, direction, flags),
+ *     getFirstConsumedCharAfter(element, direction, flags),
+ * )
+ * ```
+ */
+export function getFirstConsumedCharPlusAfter(
+    element: Element | Alternative,
+    direction: MatchingDirection,
+    flags: ReadonlyFlags,
+): FirstConsumedChar {
+    const consumed = getFirstConsumedChar(element, direction, flags)
+
+    if (!consumed.empty) {
+        return consumed
+    }
+
+    return FirstConsumedChars.concat(
+        [consumed, getFirstConsumedCharAfter(element, direction, flags)],
+        flags,
+    )
+}
diff --git a/lib/utils/regexp-ast/index.ts b/lib/utils/regexp-ast/index.ts
index c156bdd..66be9bd 100644
--- a/lib/utils/regexp-ast/index.ts
+++ b/lib/utils/regexp-ast/index.ts
@@ -11,7 +11,7 @@ import {
     isEmptyBackreference,
     toCharSet,
 } from "regexp-ast-analysis"
-export { ShortCircuit } from "./common"
+export { ShortCircuit, getFirstConsumedCharPlusAfter } from "./common"
 export * from "./is-covered"
 export * from "./is-equals"
 
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index f332a51..f8dedc8 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -11,6 +11,7 @@ import noDupeCharactersCharacterClass from "../rules/no-dupe-characters-characte
 import noDupeDisjunctions from "../rules/no-dupe-disjunctions"
 import noEmptyAlternative from "../rules/no-empty-alternative"
 import noEmptyCapturingGroup from "../rules/no-empty-capturing-group"
+import noEmptyCharacterClass from "../rules/no-empty-character-class"
 import noEmptyGroup from "../rules/no-empty-group"
 import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
 import noEscapeBackspace from "../rules/no-escape-backspace"
@@ -49,7 +50,9 @@ import orderInCharacterClass from "../rules/order-in-character-class"
 import preferCharacterClass from "../rules/prefer-character-class"
 import preferD from "../rules/prefer-d"
 import preferEscapeReplacementDollarChar from "../rules/prefer-escape-replacement-dollar-char"
+import preferLookaround from "../rules/prefer-lookaround"
 import preferNamedBackreference from "../rules/prefer-named-backreference"
+import preferNamedCaptureGroup from "../rules/prefer-named-capture-group"
 import preferPlusQuantifier from "../rules/prefer-plus-quantifier"
 import preferPredefinedAssertion from "../rules/prefer-predefined-assertion"
 import preferQuantifier from "../rules/prefer-quantifier"
@@ -61,6 +64,7 @@ import preferStarQuantifier from "../rules/prefer-star-quantifier"
 import preferT from "../rules/prefer-t"
 import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes"
 import preferW from "../rules/prefer-w"
+import requireUnicodeRegexp from "../rules/require-unicode-regexp"
 import sortAlternatives from "../rules/sort-alternatives"
 import sortCharacterClassElements from "../rules/sort-character-class-elements"
 import sortFlags from "../rules/sort-flags"
@@ -80,6 +84,7 @@ export const rules = [
     noDupeDisjunctions,
     noEmptyAlternative,
     noEmptyCapturingGroup,
+    noEmptyCharacterClass,
     noEmptyGroup,
     noEmptyLookaroundsAssertion,
     noEscapeBackspace,
@@ -118,7 +123,9 @@ export const rules = [
     preferCharacterClass,
     preferD,
     preferEscapeReplacementDollarChar,
+    preferLookaround,
     preferNamedBackreference,
+    preferNamedCaptureGroup,
     preferPlusQuantifier,
     preferPredefinedAssertion,
     preferQuantifier,
@@ -130,6 +137,7 @@ export const rules = [
     preferT,
     preferUnicodeCodepointEscapes,
     preferW,
+    requireUnicodeRegexp,
     sortAlternatives,
     sortCharacterClassElements,
     sortFlags,
diff --git a/tools/update-rulesets.ts b/tools/update-rulesets.ts
index 6114701..82d245c 100644
--- a/tools/update-rulesets.ts
+++ b/tools/update-rulesets.ts
@@ -24,6 +24,8 @@ export = {
         // The ESLint rule will report fewer cases than our rule
         "no-invalid-regexp": "off",
         "no-useless-backreference": "off",
+        // TODO Switch in the major version.
+        // "no-empty-character-class": "off",
 
         // eslint-plugin-regexp rules
         ${rules


Test patch:
diff --git a/tests/lib/rules/no-empty-character-class.ts b/tests/lib/rules/no-empty-character-class.ts
new file mode 100644
index 0000000..8b5e229
--- /dev/null
+++ b/tests/lib/rules/no-empty-character-class.ts
@@ -0,0 +1,79 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-empty-character-class"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: "module",
+    },
+})
+
+tester.run("no-empty-character-class", rule as any, {
+    valid: [
+        `/[a]/`,
+        `/[a-z]/`,
+        `/[a]?/`,
+        `/[a]*/`,
+        `/[[]/`,
+        String.raw`/\[]/`,
+        `/[^]/`,
+        `/[()]/`,
+        `/[ ]/`,
+        String.raw`/[\s\S]/`,
+        String.raw`/[\da-zA-Z_\W]/`,
+    ],
+    invalid: [
+        {
+            code: `/[]/`,
+            errors: [
+                {
+                    message:
+                        "This character class matches no characters because it is empty.",
+                    line: 1,
+                    column: 2,
+                },
+            ],
+        },
+        {
+            code: `/abc[]/`,
+            errors: [
+                {
+                    message:
+                        "This character class matches no characters because it is empty.",
+                    line: 1,
+                    column: 5,
+                },
+            ],
+        },
+        {
+            code: `/([])/`,
+            errors: [
+                {
+                    message:
+                        "This character class matches no characters because it is empty.",
+                    line: 1,
+                    column: 3,
+                },
+            ],
+        },
+        {
+            code: `new RegExp("[]");`,
+            errors: [
+                {
+                    message:
+                        "This character class matches no characters because it is empty.",
+                    line: 1,
+                    column: 13,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[^\s\S]/`,
+            errors: ["This character class cannot match any characters."],
+        },
+        {
+            code: String.raw`/[^\da-zA-Z_\W]/`,
+            errors: ["This character class cannot match any characters."],
+        },
+    ],
+})
diff --git a/tests/lib/rules/no-useless-flag.ts b/tests/lib/rules/no-useless-flag.ts
index f76f124..6c44226 100644
--- a/tests/lib/rules/no-useless-flag.ts
+++ b/tests/lib/rules/no-useless-flag.ts
@@ -845,6 +845,7 @@ describe("Don't conflict even if using the rules together.", () => {
             output: String.raw`/[\s\S]*/;`,
             rulesConfig: {
                 "regexp/no-useless-flag": ["error"],
+                "regexp/require-unicode-regexp": "off",
                 "regexp/match-any": ["error", { allows: ["dotAll"] }],
             },
             messages: [
@@ -872,6 +873,7 @@ describe("Don't conflict even if using the rules together.", () => {
             output: String.raw`/[\s\S]*/;`,
             rulesConfig: {
                 "regexp/match-any": ["error", { allows: ["dotAll"] }],
+                "regexp/require-unicode-regexp": "off",
                 "regexp/no-useless-flag": ["error"],
             },
             messages: [
diff --git a/tests/lib/rules/prefer-lookaround.ts b/tests/lib/rules/prefer-lookaround.ts
new file mode 100644
index 0000000..155e969
--- /dev/null
+++ b/tests/lib/rules/prefer-lookaround.ts
@@ -0,0 +1,441 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/prefer-lookaround"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: "module",
+    },
+})
+
+tester.run("prefer-lookaround", rule as any, {
+    valid: [
+        `const str = 'I love unicorn! I hate unicorn?'.replace(/(?<=love )unicorn(?=!)/, '🦄');`,
+        `const str = 'A JavaScript linter written in JavaScript.'.replaceAll(/Java(?=Script)/g, 'Type');`,
+        `const str = 'A JavaScript formatter written in JavaScript.'.replace(/(?<=Java)Script/g, '');`,
+        `'JavaScript.'.replace(/(Java)(Script)/, '$1, $1$2, and Type$2')`,
+        `'JavaScript.'.replace(/Java(Script)/, 'Java$1 and Type$1')`,
+        `'JavaScript.'.replace(/Java(?<script>Script)/, 'Java$<script> and Type$1')`,
+        `'JavaScript.'.replace(/(JavaScript)/, '$1')`,
+        `'JavaScript.'.replace(/Java(Script)/, '$1$1')`,
+        `'JavaScript.'.replace(/Java(Script)/, '$1foo$1')`,
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, '$1$2');
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            `,
+            options: [{ strictTypes: true }],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, '$1$2');
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            useUnknown(regex)
+            `,
+            options: [{ strictTypes: false }],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, '$1$2');
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            regex.test(foo)
+            `,
+            options: [{ strictTypes: false }],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, '$1');
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            `,
+            options: [{ strictTypes: false }],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, unknown);
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            `,
+            options: [{ strictTypes: false }],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, 42);
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            `,
+            options: [{ strictTypes: false }],
+        },
+        `
+        "aa-a".replace(/a(\\b|$)/g, 'b$1')
+        "aaa".replace(/(^)a/, '$1b')
+        `,
+        `
+        "aaaaaa".replace(/(a)a(a)/g, "$1b$2")
+        // 'abaaba'
+        "aaaaaa".replace(/(?<=a)a(?=a)/g, "b")
+        // 'abbbba'
+        `,
+        `
+        "aaaaaa".replace(/(^a+)a(?=a)/, "$1b")
+        // 'aaaaba'
+        "aaaaaa".replace(/(?<=^a+)a(?=a)/, "b")
+        // 'abaaaa'
+        `,
+        `
+        "aaaaaa".replace(/(a)a/g, "$1b")
+        // 'ababab'
+        "aaaaaa".replace(/(?<=a)a/g, "b")
+        // 'abbbbb'
+        `,
+        `
+        "aaaaaa".replace(/(a)\\w/g, "$1b")
+        // 'ababab'
+        "aaaaaa".replace(/(?<=a)\\w/g, "b")
+        // 'abbbbb'
+        `,
+        `
+        "aaaaaa".replace(/(\\w)a/g, "$1b")
+        // 'ababab'
+        "aaaaaa".replace(/(?<=\\w)a/g, "b")
+        // 'abbbbb'
+        `,
+        `
+        "aaaaaa".replace(/(aa)a/g, "$1b")
+        // 'aabaab'
+        "aaaaaa".replace(/(?<=aa)a/g, "b")
+        // 'aabbbb'
+        `,
+        `
+        "aaaaaa".replace(/(a)aa/g, "$1bb")
+        // 'abbabb'
+        "aaaaaa".replace(/(?<=a)aa/g, "bb")
+        // 'abbbba'
+        `,
+        `
+        "aaaaaa".replace(/(a)a*/g, "$1b")
+        // 'ab'
+        "aaaaaa".replace(/(?<=a)a*/g, "b")
+        // 'abb'
+        `,
+        `
+        "aaaaaa".replace(/(a*)a/g, "$1b")
+        // 'aaaaab'
+        "aaaaaa".replace(/(?<=a*)a/g, "b")
+        // 'bbbbbb'
+        `,
+        `
+        "aaaaaa".replace(/(^a+)a/, "$1b")
+        // 'aaaaab'
+        "aaaaaa".replace(/(?<=^a+)a/, "b")
+        // 'abaaaa'
+        `,
+        `
+        "aaaaaa".replace(/a(a+)/g, "b$1")
+        // 'baaaaa'
+        "aaaaaa".replace(/a(?=a+)/g, "b")
+        // 'bbbbba'
+        `,
+        `
+        "ababababa".replace(/(a)b(a)/g, "$1c$2")
+        // 'acabacaba'
+        "ababababa".replace(/(?<=a)b(?=a)/g, "c")
+        // 'acacacaca'
+        `,
+        `
+        "aaaaaaaaa".replace(/(a{0,3})a/g, "$1b")
+        // 'aaabaaabb'
+        "aaaaaaaaa".replace(/(?<=a{0,3})a/g, "b")
+        // 'bbbbbbbbb'
+        `,
+        `
+        "aaaaaaaaa".replace(/(a{0,3})aaaaa/g, "$1b")
+        // 'aaaba'
+        "aaaaaaaaa".replace(/(?<=a{0,3})aaaaa/g, "b")
+        // 'baaaa'
+        `,
+        `
+        "aaaab".replace(/(a+)a+b/, "$1c")
+        // 'aaac'
+        "aaaab".replace(/(?<=a+)a+b/, "c")
+        // 'ac'
+        `,
+        `
+        "abaaaba".replace(/ab(a+)/g, "c$1")
+        // 'caaaba'
+        "abaaaba".replace(/ab(?=a+)/g, "c")
+        // 'caaca'
+        `,
+        `
+        "ababababa".replace(/(a)ba/g, "$1c")
+        // 'acbacba'
+        "ababababa".replace(/(?<=a)ba/g, "c")
+        // 'acccc'
+        `,
+        `
+        "aJavaJava".replace(/(a)Java/g, '$1foo')
+        // 'afooJava'
+        "aJavaJava".replace(/(?<=a)Java/g, 'foo')
+        // 'afoofoo'
+        `,
+        `
+        "aaaaaa".replace(/(a)a(?:)a/g, "$1bb")
+        // 'abbabb'
+        "aaaaaa".replace(/(?<=a)a(?:)a/g, "bb")
+        // 'abbbba'
+        `,
+        `
+        "aaaaaa".replace(/(a)aa(?:)/g, "$1bb")
+        // 'abbabb'
+        "aaaaaa".replace(/(?<=a)aa(?:)/g, "bb")
+        // 'abbbba'
+        `,
+    ],
+    invalid: [
+        {
+            code: `
+            const str = 'I love unicorn! I hate unicorn?'.replace(/(?<before>love )unicorn(?<after>!)/, '$<before>🦄$<after>');
+            `,
+            output: `
+            const str = 'I love unicorn! I hate unicorn?'.replace(/(?<=love )unicorn(?=!)/, '🦄');
+            `,
+            errors: [
+                {
+                    message:
+                        "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+                    column: 68,
+                    endColumn: 84,
+                },
+                {
+                    message:
+                        "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+                    column: 91,
+                    endColumn: 102,
+                },
+            ],
+        },
+        {
+            code: `
+            const str = 'I love unicorn! I hate unicorn?'.replace(/(love )unicorn(!)/, '$1🦄$2');
+            `,
+            output: `
+            const str = 'I love unicorn! I hate unicorn?'.replace(/(?<=love )unicorn(?=!)/, '🦄');
+            `,
+            errors: [
+                {
+                    message:
+                        "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+                    column: 68,
+                    endColumn: 75,
+                },
+                {
+                    message:
+                        "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+                    column: 82,
+                    endColumn: 85,
+                },
+            ],
+        },
+        {
+            code: `
+            const str = 'A JavaScript linter written in JavaScript.'.replaceAll(/Java(Script)/g, 'Type$1');
+            `,
+            output: `
+            const str = 'A JavaScript linter written in JavaScript.'.replaceAll(/Java(?=Script)/g, 'Type');
+            `,
+            errors: [
+                {
+                    message:
+                        "This capturing group can be replaced with a lookahead assertion ('(?=Script)').",
+                    column: 86,
+                    endColumn: 94,
+                },
+            ],
+        },
+        {
+            code: `
+            const str = 'A JavaScript formatter written in JavaScript.'.replace(/(Java)Script/g, '$1');
+            `,
+            output: `
+            const str = 'A JavaScript formatter written in JavaScript.'.replace(/(?<=Java)Script/g, '');
+            `,
+            errors: [
+                {
+                    message:
+                        "This capturing group can be replaced with a lookbehind assertion ('(?<=Java)').",
+                    column: 82,
+                    endColumn: 88,
+                },
+            ],
+        },
+        {
+            code: `
+            const str = 'JavaScript'.replace(/Java(Script)/g, '$1');
+            `,
+            output: `
+            const str = 'JavaScript'.replace(/Java(?=Script)/g, '');
+            `,
+            errors: [
+                "This capturing group can be replaced with a lookahead assertion ('(?=Script)').",
+            ],
+        },
+        {
+            code: `
+            const str = 'JavaScript.'.replace(/(Java)(Script)/, '$1 and Type$2');
+            `,
+            output: `
+            const str = 'JavaScript.'.replace(/(?<=Java)(?=Script)/, ' and Type');
+            `,
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=Java)' and '(?=Script)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=Java)' and '(?=Script)').",
+            ],
+        },
+        {
+            code: `
+            const replacement = '$<before>🦄$<after>'
+            const str = 'I love unicorn! I hate unicorn?'.replace(/(?<before>love )unicorn(?<after>!)/, replacement);
+            `,
+            output: null,
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+            ],
+        },
+        {
+            code: `
+            const replacement = '$1🦄$2'
+            const str = 'I love unicorn! I hate unicorn?'.replace(/(love )unicorn(!)/, replacement);
+            `,
+            output: null,
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=love )' and '(?=!)').",
+            ],
+        },
+        {
+            code: `
+            const str = 'JavaScript.'.replace(/(J)(ava)(Script)/, '$1Query, and Java$3');
+            `,
+            output: `
+            const str = 'JavaScript.'.replace(/(?<=J)(ava)(?=Script)/, 'Query, and Java');
+            `,
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=J)' and '(?=Script)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=J)' and '(?=Script)').",
+            ],
+        },
+        {
+            code: `
+            const str = 'JavaScript.'.replace(/(J)(ava)(Script)/, '$1Query, $2, and Java$3');
+            `,
+            output: null,
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=J)' and '(?=Script)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=J)' and '(?=Script)').",
+            ],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/g
+            str.replace(regex, '$1$2');
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            `,
+            output: `
+            const regex = /(?<=Java)(?=Script)/g
+            str.replace(regex, '');
+            str2.replace(regex, ' and Java');
+            str3.replace(regex, ' and ');
+            `,
+            options: [{ strictTypes: false }],
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=Java)' and '(?=Script)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=Java)' and '(?=Script)').",
+            ],
+        },
+        {
+            code: `
+            const regex = /(Java)(Script)/
+            str.replace(regex, '$1$2');
+            str2.replace(regex, '$1 and Java$2');
+            str3.replace(regex, '$1 and $2');
+            regex.test(str);
+            `,
+            output: `
+            const regex = /(?<=Java)(?=Script)/
+            str.replace(regex, '');
+            str2.replace(regex, ' and Java');
+            str3.replace(regex, ' and ');
+            regex.test(str);
+            `,
+            options: [{ strictTypes: false }],
+            errors: [
+                "These capturing groups can be replaced with lookaround assertions ('(?<=Java)' and '(?=Script)').",
+                "These capturing groups can be replaced with lookaround assertions ('(?<=Java)' and '(?=Script)').",
+            ],
+        },
+        {
+            code: `
+            const regex = /(a)b(c)/
+            "abc".replace(regex, '$1ccount');
+            "abc".replace(regex, '$1$2$2ount');
+            `,
+            output: null,
+            errors: [
+                "This capturing group can be replaced with a lookbehind assertion ('(?<=a)').",
+            ],
+        },
+        {
+            code: `
+            const regex = /(a)b(c)/
+            "abc".replace(regex, 'dynami$2');
+            "abc".replace(regex, 'dyn$1mi$2');
+            `,
+            output: `
+            const regex = /(a)b(?=c)/
+            "abc".replace(regex, 'dynami');
+            "abc".replace(regex, 'dyn$1mi');
+            `,
+            errors: [
+                "This capturing group can be replaced with a lookahead assertion ('(?=c)').",
+            ],
+        },
+        {
+            code: `
+            "aabcaabbcc".replace(/(a+)b+c/g, "$1_")
+            // 'aa_aa_c'
+            `,
+            output: `
+            "aabcaabbcc".replace(/(?<=a+)b+c/g, "_")
+            // 'aa_aa_c'
+            `,
+            errors: [
+                "This capturing group can be replaced with a lookbehind assertion ('(?<=a+)').",
+            ],
+        },
+        {
+            code: `"aaaaaa".replace(/(^a+)a(a)/, "$1b$2")`,
+            output: `"aaaaaa".replace(/(^a+)a(?=a)/, "$1b")`,
+            errors: [
+                "This capturing group can be replaced with a lookahead assertion ('(?=a)').",
+            ],
+        },
+        {
+            code: `"aaaaaa".replace(/(^a+)a((a))/, "$1b$3$2")`,
+            output: null,
+            errors: [
+                "This capturing group can be replaced with a lookahead assertion ('(?=(a))').",
+            ],
+        },
+    ],
+})
diff --git a/tests/lib/rules/prefer-named-capture-group.ts b/tests/lib/rules/prefer-named-capture-group.ts
new file mode 100644
index 0000000..f99318e
--- /dev/null
+++ b/tests/lib/rules/prefer-named-capture-group.ts
@@ -0,0 +1,26 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/prefer-named-capture-group"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: "module",
+    },
+})
+
+tester.run("prefer-named-capture-group", rule as any, {
+    valid: [
+        String.raw`/foo/`,
+        String.raw`/b(?:a(?:r))/`,
+        String.raw`/(?<foo>bar)/`,
+        String.raw`/(?=a)(?<=b)/`,
+    ],
+    invalid: [
+        {
+            code: String.raw`/(foo)/`,
+            errors: [
+                "Capture group '(foo)' should be converted to a named or non-capturing group.",
+            ],
+        },
+    ],
+})
diff --git a/tests/lib/rules/require-unicode-regexp.ts b/tests/lib/rules/require-unicode-regexp.ts
new file mode 100644
index 0000000..de8190a
--- /dev/null
+++ b/tests/lib/rules/require-unicode-regexp.ts
@@ -0,0 +1,136 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/require-unicode-regexp"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: "module",
+    },
+})
+
+tester.run("require-unicode-regexp", rule as any, {
+    valid: [
+        String.raw`/foo/u`,
+        String.raw`/foo/gimuy`,
+        String.raw`RegExp('', 'u')`,
+        String.raw`new RegExp('', 'u')`,
+        String.raw`RegExp('', 'gimuy')`,
+        String.raw`new RegExp('', 'gimuy')`,
+        String.raw`const flags = 'u'; new RegExp('', flags)`,
+        String.raw`const flags = 'g'; new RegExp('', flags + 'u')`,
+        String.raw`const flags = 'gimu'; new RegExp('foo', flags[3])`,
+        String.raw`new RegExp('', flags)`,
+        String.raw`function f(flags) { return new RegExp('', flags) }`,
+        String.raw`function f(RegExp) { return new RegExp('foo') }`,
+        String.raw`new globalThis.RegExp('foo')`,
+        String.raw`new globalThis.RegExp('foo', 'u')`,
+        String.raw`globalThis.RegExp('foo', 'u')`,
+        String.raw`const flags = 'u'; new globalThis.RegExp('', flags)`,
+        String.raw`const flags = 'g'; new globalThis.RegExp('', flags + 'u')`,
+        String.raw`const flags = 'gimu'; new globalThis.RegExp('foo', flags[3])`,
+    ],
+    invalid: [
+        {
+            code: String.raw`/foo/`,
+            output: String.raw`/foo/u`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`/foo/gimy`,
+            output: String.raw`/foo/gimyu`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`RegExp('foo')`,
+            output: String.raw`RegExp('foo', "u")`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`RegExp('foo', '')`,
+            output: String.raw`RegExp('foo', 'u')`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`RegExp('foo', 'gimy')`,
+            output: String.raw`RegExp('foo', 'gimyu')`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`new RegExp('foo')`,
+            output: String.raw`new RegExp('foo', "u")`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`new RegExp('foo', '')`,
+            output: String.raw`new RegExp('foo', 'u')`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`new RegExp('foo', 'gimy')`,
+            output: String.raw`new RegExp('foo', 'gimyu')`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`const flags = 'gi'; new RegExp('foo', flags)`,
+            output: String.raw`const flags = 'giu'; new RegExp('foo', flags)`,
+            errors: [{ messageId: "require" }],
+        },
+        {
+            code: String.raw`const flags = 'gimu'; new RegExp('foo', flags[0])`,
+            output: null,
+            errors: [{ messageId: "require" }],
+        },
+
+        // Compatibility
+        // All of the below cases are only fixable if they are compatible
+
+        {
+            code: String.raw`/foo/`,
+            output: String.raw`/foo/u`,
+            errors: 1,
+        },
+        {
+            code: String.raw`/❤️/`,
+            output: String.raw`/❤️/u`,
+            errors: 1,
+        },
+        {
+            code: String.raw`/foo/i`,
+            output: String.raw`/foo/iu`,
+            errors: 1,
+        },
+        {
+            // "k" maps to 3 characters in ignore-case Unicode mode
+            code: String.raw`/k/i`,
+            output: null,
+            errors: 1,
+        },
+        {
+            // Same for \w
+            code: String.raw`/\w/i`,
+            output: null,
+            errors: 1,
+        },
+        {
+            // Same for \b
+            code: String.raw`/\bfoo/i`,
+            output: null,
+            errors: 1,
+        },
+        {
+            code: String.raw`/[😃]/`,
+            output: null,
+            errors: 1,
+        },
+        {
+            code: String.raw`/😃+/`,
+            output: null,
+            errors: 1,
+        },
+        {
+            code: String.raw`/\p{Ll}/`,
+            output: null,
+            errors: 1,
+        },
+    ],
+})
diff --git a/tests/lib/rules/sort-flags.ts b/tests/lib/rules/sort-flags.ts
index cabb6fe..00ae6d1 100644
--- a/tests/lib/rules/sort-flags.ts
+++ b/tests/lib/rules/sort-flags.ts
@@ -23,6 +23,7 @@ tester.run("sort-flags", rule as any, {
         String.raw`
         const flags = "yusimg"
         new RegExp("\\w", flags)
+        new RegExp("\\w", flags)
         `,
     ],
     invalid: [