Skip to content

Commit cd75295

Browse files
authored
Add regexp/no-missing-g-flag rule (#474)
1 parent ce01905 commit cd75295

File tree

6 files changed

+457
-0
lines changed

6 files changed

+457
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
116116
| [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: |
117117
| [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: |
118118
| [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: |
119+
| [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: |
119120
| [regexp/no-optional-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-optional-assertion.html) | disallow optional assertions | :star: |
120121
| [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: |
121122
| [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: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2525
| [regexp/no-invalid-regexp](./no-invalid-regexp.md) | disallow invalid regular expression strings in `RegExp` constructors | :star: |
2626
| [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | :star: |
2727
| [regexp/no-misleading-unicode-character](./no-misleading-unicode-character.md) | disallow multi-code-point characters in character classes and quantifiers | :wrench: |
28+
| [regexp/no-missing-g-flag](./no-missing-g-flag.md) | disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll` | :wrench: |
2829
| [regexp/no-optional-assertion](./no-optional-assertion.md) | disallow optional assertions | :star: |
2930
| [regexp/no-potentially-useless-backreference](./no-potentially-useless-backreference.md) | disallow backreferences that reference a group that might not be matched | :star: |
3031
| [regexp/no-super-linear-backtracking](./no-super-linear-backtracking.md) | disallow exponential and polynomial backtracking | :star::wrench: |

docs/rules/no-missing-g-flag.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-missing-g-flag"
5+
description: "disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`"
6+
---
7+
# regexp/no-missing-g-flag
8+
9+
> disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
## :book: Rule Details
15+
16+
When calling [`String#matchAll()`] and [`String#replaceAll()`] with a `RegExp` missing the global (`g`) flag, it will be a runtime error.
17+
This rule reports `RegExp`s missing the global (`g`) flag that cause these errors.
18+
19+
<eslint-code-block fix>
20+
21+
```js
22+
/* eslint regexp/no-missing-g-flag: "error" */
23+
const games = 'table football, foosball'
24+
const text = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?';
25+
26+
/* ✓ GOOD */
27+
var m = games.matchAll(/foo/g);
28+
var newText = text.replaceAll(/Dog/ig, 'cat');
29+
30+
31+
/* ✗ BAD */
32+
var m = games.matchAll(/foo/);
33+
var newText = text.replaceAll(/Dog/i, 'cat');
34+
35+
```
36+
37+
</eslint-code-block>
38+
39+
## :wrench: Options
40+
41+
```json
42+
{
43+
"regexp/no-missing-g-flag": ["error",
44+
{
45+
"strictTypes": true
46+
}
47+
]
48+
}
49+
```
50+
51+
- `strictTypes` ... If `true`, strictly check the type of object to determine if the regex instance was used in `matchAll()` and `replaceAll()`. Default is `true`.
52+
This option is always on when using TypeScript.
53+
54+
## :books: Further reading
55+
56+
- [MDN Web Docs - String.prototype.matchAll()][`String#matchAll()`]
57+
- [MDN Web Docs - String.prototype.replaceAll()][`String#replaceAll()`]
58+
59+
[`String#matchAll()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
60+
[`String#replaceAll()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
61+
62+
## :mag: Implementation
63+
64+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-missing-g-flag.ts)
65+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-missing-g-flag.ts)

lib/rules/no-missing-g-flag.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { RegExpContext, UnparsableRegExpContext } from "../utils"
2+
import { createRule, defineRegexpVisitor } from "../utils"
3+
import type { ExpressionReference } from "../utils/ast-utils"
4+
import {
5+
extractExpressionReferences,
6+
isKnownMethodCall,
7+
} from "../utils/ast-utils"
8+
import { createTypeTracker } from "../utils/type-tracker"
9+
10+
/**
11+
* Parse option
12+
*/
13+
function parseOption(
14+
userOption:
15+
| {
16+
strictTypes?: boolean
17+
}
18+
| undefined,
19+
) {
20+
let strictTypes = true
21+
if (userOption) {
22+
if (userOption.strictTypes != null) {
23+
strictTypes = userOption.strictTypes
24+
}
25+
}
26+
27+
return {
28+
strictTypes,
29+
}
30+
}
31+
32+
export default createRule("no-missing-g-flag", {
33+
meta: {
34+
docs: {
35+
description:
36+
"disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`",
37+
category: "Possible Errors",
38+
// TODO Switch to recommended in the major version.
39+
// recommended: true,
40+
recommended: false,
41+
},
42+
fixable: "code",
43+
schema: [
44+
{
45+
type: "object",
46+
properties: {
47+
strictTypes: { type: "boolean" },
48+
},
49+
additionalProperties: false,
50+
},
51+
],
52+
messages: {
53+
missingGlobalFlag:
54+
"The pattern given to the argument of `String#{{method}}()` requires the `g` flag, but is missing it.",
55+
},
56+
type: "problem",
57+
},
58+
create(context) {
59+
const { strictTypes } = parseOption(context.options[0])
60+
const typeTracer = createTypeTracker(context)
61+
62+
/** The logic of this rule */
63+
function visit(regexpContext: RegExpContext | UnparsableRegExpContext) {
64+
const { regexpNode, flags, flagsString } = regexpContext
65+
66+
if (
67+
flags.global ||
68+
// We were unable to determine which flags were used.
69+
flagsString == null
70+
) {
71+
return
72+
}
73+
74+
for (const ref of extractExpressionReferences(
75+
regexpNode,
76+
context,
77+
)) {
78+
verifyExpressionReference(ref, regexpContext)
79+
}
80+
}
81+
82+
/**
83+
* Verify RegExp reference
84+
*/
85+
function verifyExpressionReference(
86+
ref: ExpressionReference,
87+
{
88+
regexpNode,
89+
fixReplaceFlags,
90+
flagsString,
91+
}: RegExpContext | UnparsableRegExpContext,
92+
): void {
93+
if (ref.type !== "argument") {
94+
// It is not used for function call arguments.
95+
return
96+
}
97+
const node = ref.callExpression
98+
if (
99+
// It is not specified in the first argument.
100+
node.arguments[0] !== ref.node ||
101+
// It is not replaceAll() and matchAll().
102+
!isKnownMethodCall(node, {
103+
matchAll: 1,
104+
replaceAll: 2,
105+
})
106+
) {
107+
return
108+
}
109+
if (
110+
strictTypes
111+
? !typeTracer.isString(node.callee.object)
112+
: !typeTracer.maybeString(node.callee.object)
113+
) {
114+
// The callee object is not a string.
115+
return
116+
}
117+
context.report({
118+
node: ref.node,
119+
messageId: "missingGlobalFlag",
120+
data: {
121+
method: node.callee.property.name,
122+
},
123+
fix: buildFixer(),
124+
})
125+
126+
/** Build fixer */
127+
function buildFixer() {
128+
if (
129+
node.arguments[0] !== regexpNode ||
130+
((regexpNode.type === "NewExpression" ||
131+
regexpNode.type === "CallExpression") &&
132+
regexpNode.arguments[1] &&
133+
regexpNode.arguments[1].type !== "Literal")
134+
) {
135+
// It can only be safely auto-fixed if it is defined directly in the argument.
136+
return null
137+
}
138+
return fixReplaceFlags(`${flagsString!}g`, false)
139+
}
140+
}
141+
142+
return defineRegexpVisitor(context, {
143+
createVisitor(regexpContext) {
144+
visit(regexpContext)
145+
return {}
146+
},
147+
visitInvalid: visit,
148+
visitUnknown: visit,
149+
})
150+
},
151+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import noInvisibleCharacter from "../rules/no-invisible-character"
2121
import noLazyEnds from "../rules/no-lazy-ends"
2222
import noLegacyFeatures from "../rules/no-legacy-features"
2323
import noMisleadingUnicodeCharacter from "../rules/no-misleading-unicode-character"
24+
import noMissingGFlag from "../rules/no-missing-g-flag"
2425
import noNonStandardFlag from "../rules/no-non-standard-flag"
2526
import noObscureRange from "../rules/no-obscure-range"
2627
import noOctal from "../rules/no-octal"
@@ -99,6 +100,7 @@ export const rules = [
99100
noLazyEnds,
100101
noLegacyFeatures,
101102
noMisleadingUnicodeCharacter,
103+
noMissingGFlag,
102104
noNonStandardFlag,
103105
noObscureRange,
104106
noOctal,

0 commit comments

Comments
 (0)