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

Issue:
Rule proposal: `no-missing-g-flag`
**Motivation**
<!-- A clear and concise description of the problem the new rule is supposed to solve. -->

When calling [`String#replaceAll()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) and [`String#matchAll()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll) with non-global RegExp, it will be a runtime error. I hope to catch this pattern during linting.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll#exceptions
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll#exceptions

**Description**
<!-- A clear and concise description of the new rule. -->

Forbid calling `String#replaceAll()` or `String#matchAll()` with non-global pattern.


**Examples**

<!-- Add a few examples of regexes that the rule does and does not report. -->

```js
/* ✓ GOOD */
foo.replaceAll(" ", '')
foo.replaceAll(/\s/g, '')
foo.matchAll(/\s/g)

/* ✗ BAD */
foo.replaceAll(/\s/, '')
foo.matchAll(/\s/)
```


Pull request:
Add `regexp/no-missing-g-flag` rule
This PR adds `regexp/no-missing-g-flag` rule.

close #472 

Patch:
diff --git a/README.md b/README.md
index fa34837..188f04e 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-invalid-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invalid-regexp.html) | disallow invalid regular expression strings in `RegExp` constructors | :star: |
 | [regexp/no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | :star: |
 | [regexp/no-misleading-unicode-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-misleading-unicode-character.html) | disallow multi-code-point characters in character classes and quantifiers | :wrench: |
+| [regexp/no-missing-g-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-missing-g-flag.html) | disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll` | :wrench: |
 | [regexp/no-optional-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-optional-assertion.html) | disallow optional assertions | :star: |
 | [regexp/no-potentially-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-potentially-useless-backreference.html) | disallow backreferences that reference a group that might not be matched | :star: |
 | [regexp/no-super-linear-backtracking](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-super-linear-backtracking.html) | disallow exponential and polynomial backtracking | :star::wrench: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index bf50b78..321ec49 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -25,6 +25,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
 | [regexp/no-invalid-regexp](./no-invalid-regexp.md) | disallow invalid regular expression strings in `RegExp` constructors | :star: |
 | [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | :star: |
 | [regexp/no-misleading-unicode-character](./no-misleading-unicode-character.md) | disallow multi-code-point characters in character classes and quantifiers | :wrench: |
+| [regexp/no-missing-g-flag](./no-missing-g-flag.md) | disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll` | :wrench: |
 | [regexp/no-optional-assertion](./no-optional-assertion.md) | disallow optional assertions | :star: |
 | [regexp/no-potentially-useless-backreference](./no-potentially-useless-backreference.md) | disallow backreferences that reference a group that might not be matched | :star: |
 | [regexp/no-super-linear-backtracking](./no-super-linear-backtracking.md) | disallow exponential and polynomial backtracking | :star::wrench: |
diff --git a/docs/rules/no-missing-g-flag.md b/docs/rules/no-missing-g-flag.md
new file mode 100644
index 0000000..c54b03a
--- /dev/null
+++ b/docs/rules/no-missing-g-flag.md
@@ -0,0 +1,65 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-missing-g-flag"
+description: "disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`"
+---
+# regexp/no-missing-g-flag
+
+> disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`
+
+- :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
+
+When calling [`String#matchAll()`] and [`String#replaceAll()`] with a `RegExp` missing the global (`g`) flag, it will be a runtime error.
+This rule reports `RegExp`s missing the global (`g`) flag that cause these errors.
+
+<eslint-code-block fix>
+
+```js
+/* eslint regexp/no-missing-g-flag: "error" */
+const games = 'table football, foosball'
+const text = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?';
+
+/* ✓ GOOD */
+var m = games.matchAll(/foo/g);
+var newText = text.replaceAll(/Dog/ig, 'cat');
+
+
+/* ✗ BAD */
+var m = games.matchAll(/foo/);
+var newText = text.replaceAll(/Dog/i, 'cat');
+
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "regexp/no-missing-g-flag": ["error",
+    {
+      "strictTypes": true
+    }
+  ]
+}
+```
+
+- `strictTypes` ... If `true`, strictly check the type of object to determine if the regex instance was used in `matchAll()` and `replaceAll()`. Default is `true`.  
+  This option is always on when using TypeScript.
+
+## :books: Further reading
+
+- [MDN Web Docs - String.prototype.matchAll()][`String#matchAll()`]
+- [MDN Web Docs - String.prototype.replaceAll()][`String#replaceAll()`]
+
+[`String#matchAll()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
+[`String#replaceAll()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-missing-g-flag.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-missing-g-flag.ts)
diff --git a/lib/rules/no-missing-g-flag.ts b/lib/rules/no-missing-g-flag.ts
new file mode 100644
index 0000000..b91d2c0
--- /dev/null
+++ b/lib/rules/no-missing-g-flag.ts
@@ -0,0 +1,151 @@
+import type { RegExpContext, UnparsableRegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import type { ExpressionReference } from "../utils/ast-utils"
+import {
+    extractExpressionReferences,
+    isKnownMethodCall,
+} from "../utils/ast-utils"
+import { createTypeTracker } from "../utils/type-tracker"
+
+/**
+ * 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("no-missing-g-flag", {
+    meta: {
+        docs: {
+            description:
+                "disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`",
+            category: "Possible Errors",
+            // TODO Switch to recommended in the major version.
+            // recommended: true,
+            recommended: false,
+        },
+        fixable: "code",
+        schema: [
+            {
+                type: "object",
+                properties: {
+                    strictTypes: { type: "boolean" },
+                },
+                additionalProperties: false,
+            },
+        ],
+        messages: {
+            missingGlobalFlag:
+                "The pattern given to the argument of `String#{{method}}()` requires the `g` flag, but is missing it.",
+        },
+        type: "problem",
+    },
+    create(context) {
+        const { strictTypes } = parseOption(context.options[0])
+        const typeTracer = createTypeTracker(context)
+
+        /** The logic of this rule */
+        function visit(regexpContext: RegExpContext | UnparsableRegExpContext) {
+            const { regexpNode, flags, flagsString } = regexpContext
+
+            if (
+                flags.global ||
+                // We were unable to determine which flags were used.
+                flagsString == null
+            ) {
+                return
+            }
+
+            for (const ref of extractExpressionReferences(
+                regexpNode,
+                context,
+            )) {
+                verifyExpressionReference(ref, regexpContext)
+            }
+        }
+
+        /**
+         * Verify RegExp reference
+         */
+        function verifyExpressionReference(
+            ref: ExpressionReference,
+            {
+                regexpNode,
+                fixReplaceFlags,
+                flagsString,
+            }: RegExpContext | UnparsableRegExpContext,
+        ): void {
+            if (ref.type !== "argument") {
+                // It is not used for function call arguments.
+                return
+            }
+            const node = ref.callExpression
+            if (
+                // It is not specified in the first argument.
+                node.arguments[0] !== ref.node ||
+                // It is not replaceAll() and matchAll().
+                !isKnownMethodCall(node, {
+                    matchAll: 1,
+                    replaceAll: 2,
+                })
+            ) {
+                return
+            }
+            if (
+                strictTypes
+                    ? !typeTracer.isString(node.callee.object)
+                    : !typeTracer.maybeString(node.callee.object)
+            ) {
+                // The callee object is not a string.
+                return
+            }
+            context.report({
+                node: ref.node,
+                messageId: "missingGlobalFlag",
+                data: {
+                    method: node.callee.property.name,
+                },
+                fix: buildFixer(),
+            })
+
+            /** Build fixer */
+            function buildFixer() {
+                if (
+                    node.arguments[0] !== regexpNode ||
+                    ((regexpNode.type === "NewExpression" ||
+                        regexpNode.type === "CallExpression") &&
+                        regexpNode.arguments[1] &&
+                        regexpNode.arguments[1].type !== "Literal")
+                ) {
+                    // It can only be safely auto-fixed if it is defined directly in the argument.
+                    return null
+                }
+                return fixReplaceFlags(`${flagsString!}g`, false)
+            }
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor(regexpContext) {
+                visit(regexpContext)
+                return {}
+            },
+            visitInvalid: visit,
+            visitUnknown: visit,
+        })
+    },
+})
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index c5c2348..43d6576 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -21,6 +21,7 @@ import noInvisibleCharacter from "../rules/no-invisible-character"
 import noLazyEnds from "../rules/no-lazy-ends"
 import noLegacyFeatures from "../rules/no-legacy-features"
 import noMisleadingUnicodeCharacter from "../rules/no-misleading-unicode-character"
+import noMissingGFlag from "../rules/no-missing-g-flag"
 import noNonStandardFlag from "../rules/no-non-standard-flag"
 import noObscureRange from "../rules/no-obscure-range"
 import noOctal from "../rules/no-octal"
@@ -99,6 +100,7 @@ export const rules = [
     noLazyEnds,
     noLegacyFeatures,
     noMisleadingUnicodeCharacter,
+    noMissingGFlag,
     noNonStandardFlag,
     noObscureRange,
     noOctal,


Test patch:
diff --git a/tests/lib/rules/no-missing-g-flag.ts b/tests/lib/rules/no-missing-g-flag.ts
new file mode 100644
index 0000000..078fe40
--- /dev/null
+++ b/tests/lib/rules/no-missing-g-flag.ts
@@ -0,0 +1,237 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-missing-g-flag"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: "module",
+    },
+})
+
+tester.run("no-missing-g-flag", rule as any, {
+    valid: [
+        `
+        const s = 'foo'
+        const ret = s.replaceAll(/foo/g, 'bar')
+        `,
+        `
+        const s = 'foo'
+        const ret = s.matchAll(/foo/g)
+        `,
+        `
+        const s = 'foo'
+        const ret = s.replaceAll(new RegExp('foo', 'g'), 'bar')
+        `,
+        `
+        const s = 'foo'
+        const ret = s.matchAll(new RegExp('foo', 'g'))
+        `,
+        // not matchAll and replaceAll
+        `
+        const s = 'foo'
+        const ret = s.replace(/foo/, 'bar')
+        `,
+        `
+        const s = 'foo'
+        const ret = s.match(/foo/)
+        `,
+        // unknown usage
+        `
+        const s = 'foo'
+        const ret = s.replaceAll('bar', /foo/)
+        `,
+        `
+        const ret = /foo/.replaceAll(p, 'bar')
+        `,
+        // unknown type
+        `
+        const ret = unknown.replaceAll(/foo/, 'bar')
+        `,
+        // unknown flags
+        `
+        const s = 'foo'
+        const ret = s.replaceAll(new RegExp('foo', unknown), 'bar')
+        `,
+    ],
+    invalid: [
+        {
+            code: `
+            const s = 'foo'
+            const ret = s.replaceAll(/foo/, 'bar')
+            `,
+            output: `
+            const s = 'foo'
+            const ret = s.replaceAll(/foo/g, 'bar')
+            `,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 38,
+                },
+            ],
+        },
+        {
+            code: `
+            const s = 'foo'
+            const ret = s.matchAll(/foo/)
+            `,
+            output: `
+            const s = 'foo'
+            const ret = s.matchAll(/foo/g)
+            `,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#matchAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 36,
+                },
+            ],
+        },
+        {
+            code: `
+            const s = 'foo'
+            const ret = s.replaceAll(new RegExp('foo'), 'bar')
+            `,
+            output: `
+            const s = 'foo'
+            const ret = s.replaceAll(new RegExp('foo', "g"), 'bar')
+            `,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 38,
+                },
+            ],
+        },
+        {
+            code: `
+            const s = 'foo'
+            const ret = s.matchAll(new RegExp('foo'))
+            `,
+            output: `
+            const s = 'foo'
+            const ret = s.matchAll(new RegExp('foo', "g"))
+            `,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#matchAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 36,
+                },
+            ],
+        },
+        // Unknown pattern
+        {
+            code: `
+            const s = 'foo'
+            const ret = s.matchAll(new RegExp(foo))
+            `,
+            output: `
+            const s = 'foo'
+            const ret = s.matchAll(new RegExp(foo, "g"))
+            `,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#matchAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 36,
+                },
+            ],
+        },
+        // Invalid pattern
+        {
+            code: `
+            const s = 'foo'
+            const ret = s.matchAll(new RegExp('{', 'u'))
+            `,
+            output: `
+            const s = 'foo'
+            const ret = s.matchAll(new RegExp('{', 'ug'))
+            `,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#matchAll()` requires the `g` flag, but is missing it.",
+                    line: 3,
+                    column: 36,
+                },
+            ],
+        },
+        // can't fix
+        {
+            code: `
+            const p = /foo/
+            const s = 'foo'
+            const ret = s.replaceAll(p, 'bar')
+            `,
+            output: null,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 4,
+                    column: 38,
+                },
+            ],
+        },
+        {
+            // Variable flag
+            code: `
+            const s = 'foo'
+            const flag = 'i'
+            const ret = s.replaceAll(new RegExp('foo', flag), 'bar')
+            `,
+            output: null,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 4,
+                    column: 38,
+                },
+            ],
+        },
+        {
+            // Object property flag
+            code: `
+            const s = 'foo'
+            const f = { flag: 'i' }
+            const ret = s.replaceAll(new RegExp('foo', f.flag), 'bar')
+            `,
+            output: null,
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 4,
+                    column: 38,
+                },
+            ],
+        },
+        // unknown type with strictTypes: false
+        {
+            code: `
+            const ret = s.replaceAll(/foo/, 'bar')
+            `,
+            output: `
+            const ret = s.replaceAll(/foo/g, 'bar')
+            `,
+            options: [{ strictTypes: false }],
+            errors: [
+                {
+                    message:
+                        "The pattern given to the argument of `String#replaceAll()` requires the `g` flag, but is missing it.",
+                    line: 2,
+                    column: 38,
+                },
+            ],
+        },
+    ],
+})