Skip to content

Commit 45c18e1

Browse files
feat: add requireFlag option to require-unicode-regexp rule (#18836)
* feat: requireFlag option * docs: add option docs * docs: precise JSON schema Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: remove extra example Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: format directive Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: remove extra example Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: format directive Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: remove extra example Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: format directive Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: remove extra example Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: format directive Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * fix: remove extra quotes Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * test: cover trailing comma * fix: only provide suggestion if flags node is string literal without escapes or template literal with no expressions and no escapes * avoid flag checks when flag-to-replace not present * check `flags` and append where flag safely not found * simplify fixer Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * docs: rationale for choosing the flags * Update docs/src/rules/require-unicode-regexp.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update docs/src/rules/require-unicode-regexp.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 5d80b59 commit 45c18e1

File tree

4 files changed

+387
-18
lines changed

4 files changed

+387
-18
lines changed

docs/src/rules/require-unicode-regexp.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,87 @@ function i(flags) {
9898
9999
:::
100100
101+
## Options
102+
103+
This rule has one object option:
104+
105+
* `"requireFlag": "u"|"v"` requires a particular Unicode regex flag
106+
107+
### requireFlag: "u"
108+
109+
The `u` flag may be preferred in environments that do not support the `v` flag.
110+
111+
Examples of **incorrect** code for this rule with the `{ "requireFlag": "u" }` option:
112+
113+
:::incorrect
114+
115+
```js
116+
/*eslint require-unicode-regexp: ["error", { "requireFlag": "u" }] */
117+
118+
const fooEmpty = /foo/;
119+
120+
const fooEmptyRegexp = new RegExp('foo');
121+
122+
const foo = /foo/v;
123+
124+
const fooRegexp = new RegExp('foo', 'v');
125+
```
126+
127+
:::
128+
129+
Examples of **correct** code for this rule with the `{ "requireFlag": "u" }` option:
130+
131+
:::correct
132+
133+
```js
134+
/*eslint require-unicode-regexp: ["error", { "requireFlag": "u" }] */
135+
136+
const foo = /foo/u;
137+
138+
const fooRegexp = new RegExp('foo', 'u');
139+
```
140+
141+
:::
142+
143+
### requireFlag: "v"
144+
145+
The `v` flag may be a better choice when it is supported because it has more
146+
features than the `u` flag (e.g., the ability to test Unicode properties of strings). It
147+
does have a stricter syntax, however (e.g., the need to escape certain
148+
characters within character classes).
149+
150+
Examples of **incorrect** code for this rule with the `{ "requireFlag": "v" }` option:
151+
152+
:::incorrect
153+
154+
```js
155+
/*eslint require-unicode-regexp: ["error", { "requireFlag": "v" }] */
156+
157+
const fooEmpty = /foo/;
158+
159+
const fooEmptyRegexp = new RegExp('foo');
160+
161+
const foo = /foo/u;
162+
163+
const fooRegexp = new RegExp('foo', 'u');
164+
```
165+
166+
:::
167+
168+
Examples of **correct** code for this rule with the `{ "requireFlag": "v" }` option:
169+
170+
:::correct
171+
172+
```js
173+
/*eslint require-unicode-regexp: ["error", { "requireFlag": "v" }] */
174+
175+
const foo = /foo/v;
176+
177+
const fooRegexp = new RegExp('foo', 'v');
178+
```
179+
180+
:::
181+
101182
## When Not To Use It
102183
103184
If you don't want to warn on regular expressions without either a `u` or a `v` flag, then it's safe to disable this rule.

lib/rules/require-unicode-regexp.js

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ const {
1818
const astUtils = require("./utils/ast-utils.js");
1919
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
2020

21+
/**
22+
* Checks whether the flag configuration should be treated as a missing flag.
23+
* @param {"u"|"v"|undefined} requireFlag A particular flag to require
24+
* @param {string} flags The regex flags
25+
* @returns {boolean} Whether the flag configuration results in a missing flag.
26+
*/
27+
function checkFlags(requireFlag, flags) {
28+
let missingFlag;
29+
30+
if (requireFlag === "v") {
31+
missingFlag = !flags.includes("v");
32+
} else if (requireFlag === "u") {
33+
missingFlag = !flags.includes("u");
34+
} else {
35+
missingFlag = !flags.includes("u") && !flags.includes("v");
36+
}
37+
38+
return missingFlag;
39+
}
40+
2141
//------------------------------------------------------------------------------
2242
// Rule Definition
2343
//------------------------------------------------------------------------------
@@ -37,31 +57,65 @@ module.exports = {
3757

3858
messages: {
3959
addUFlag: "Add the 'u' flag.",
40-
requireUFlag: "Use the 'u' flag."
60+
addVFlag: "Add the 'v' flag.",
61+
requireUFlag: "Use the 'u' flag.",
62+
requireVFlag: "Use the 'v' flag."
4163
},
4264

43-
schema: []
65+
schema: [
66+
{
67+
type: "object",
68+
properties: {
69+
requireFlag: {
70+
enum: ["u", "v"]
71+
}
72+
},
73+
additionalProperties: false
74+
}
75+
]
4476
},
4577

4678
create(context) {
4779

4880
const sourceCode = context.sourceCode;
4981

82+
const {
83+
requireFlag
84+
} = context.options[0] ?? {};
85+
5086
return {
5187
"Literal[regex]"(node) {
5288
const flags = node.regex.flags || "";
5389

54-
if (!flags.includes("u") && !flags.includes("v")) {
90+
const missingFlag = checkFlags(requireFlag, flags);
91+
92+
if (missingFlag) {
5593
context.report({
56-
messageId: "requireUFlag",
94+
messageId: requireFlag === "v" ? "requireVFlag" : "requireUFlag",
5795
node,
58-
suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)
96+
suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern, requireFlag)
5997
? [
6098
{
6199
fix(fixer) {
62-
return fixer.insertTextAfter(node, "u");
100+
const replaceFlag = requireFlag ?? "u";
101+
const regex = sourceCode.getText(node);
102+
const slashPos = regex.lastIndexOf("/");
103+
104+
if (requireFlag) {
105+
const flag = requireFlag === "u" ? "v" : "u";
106+
107+
if (regex.includes(flag, slashPos)) {
108+
return fixer.replaceText(
109+
node,
110+
regex.slice(0, slashPos) +
111+
regex.slice(slashPos).replace(flag, requireFlag)
112+
);
113+
}
114+
}
115+
116+
return fixer.insertTextAfter(node, replaceFlag);
63117
},
64-
messageId: "addUFlag"
118+
messageId: requireFlag === "v" ? "addVFlag" : "addUFlag"
65119
}
66120
]
67121
: null
@@ -85,22 +139,49 @@ module.exports = {
85139
const pattern = getStringIfConstant(patternNode, scope);
86140
const flags = getStringIfConstant(flagsNode, scope);
87141

88-
if (!flagsNode || (typeof flags === "string" && !flags.includes("u") && !flags.includes("v"))) {
142+
let missingFlag = !flagsNode;
143+
144+
if (typeof flags === "string") {
145+
missingFlag = checkFlags(requireFlag, flags);
146+
}
147+
148+
if (missingFlag) {
89149
context.report({
90-
messageId: "requireUFlag",
150+
messageId: requireFlag === "v" ? "requireVFlag" : "requireUFlag",
91151
node: refNode,
92-
suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)
152+
suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern, requireFlag)
93153
? [
94154
{
95155
fix(fixer) {
156+
const replaceFlag = requireFlag ?? "u";
157+
96158
if (flagsNode) {
97159
if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") {
98160
const flagsNodeText = sourceCode.getText(flagsNode);
161+
const flag = requireFlag === "u" ? "v" : "u";
162+
163+
if (flags.includes(flag)) {
164+
165+
// Avoid replacing "u" in escapes like `\uXXXX`
166+
if (flagsNode.type === "Literal" && flagsNode.raw.includes("\\")) {
167+
return null;
168+
}
169+
170+
// Avoid replacing "u" in expressions like "`${regularFlags}g`"
171+
if (flagsNode.type === "TemplateLiteral" && (
172+
flagsNode.expressions.length ||
173+
flagsNode.quasis.some(({ value: { raw } }) => raw.includes("\\"))
174+
)) {
175+
return null;
176+
}
177+
178+
return fixer.replaceText(flagsNode, flagsNodeText.replace(flag, replaceFlag));
179+
}
99180

100181
return fixer.replaceText(flagsNode, [
101182
flagsNodeText.slice(0, flagsNodeText.length - 1),
102183
flagsNodeText.slice(flagsNodeText.length - 1)
103-
].join("u"));
184+
].join(replaceFlag));
104185
}
105186

106187
// We intentionally don't suggest concatenating + "u" to non-literals
@@ -112,11 +193,11 @@ module.exports = {
112193
return fixer.insertTextAfter(
113194
penultimateToken,
114195
astUtils.isCommaToken(penultimateToken)
115-
? ' "u",'
116-
: ', "u"'
196+
? ` "${replaceFlag}",`
197+
: `, "${replaceFlag}"`
117198
);
118199
},
119-
messageId: "addUFlag"
200+
messageId: requireFlag === "v" ? "addVFlag" : "addUFlag"
120201
}
121202
]
122203
: null

lib/rules/utils/regular-expressions.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ const REGEXPP_LATEST_ECMA_VERSION = 2025;
1414
* Checks if the given regular expression pattern would be valid with the `u` flag.
1515
* @param {number} ecmaVersion ECMAScript version to parse in.
1616
* @param {string} pattern The regular expression pattern to verify.
17+
* @param {"u"|"v"} flag The type of Unicode flag
1718
* @returns {boolean} `true` if the pattern would be valid with the `u` flag.
1819
* `false` if the pattern would be invalid with the `u` flag or the configured
1920
* ecmaVersion doesn't support the `u` flag.
2021
*/
21-
function isValidWithUnicodeFlag(ecmaVersion, pattern) {
22-
if (ecmaVersion <= 5) { // ecmaVersion <= 5 doesn't support the 'u' flag
22+
function isValidWithUnicodeFlag(ecmaVersion, pattern, flag = "u") {
23+
if (flag === "u" && ecmaVersion <= 5) { // ecmaVersion <= 5 doesn't support the 'u' flag
24+
return false;
25+
}
26+
if (flag === "v" && ecmaVersion <= 2023) {
2327
return false;
2428
}
2529

@@ -28,7 +32,11 @@ function isValidWithUnicodeFlag(ecmaVersion, pattern) {
2832
});
2933

3034
try {
31-
validator.validatePattern(pattern, void 0, void 0, { unicode: /* uFlag = */ true });
35+
validator.validatePattern(pattern, void 0, void 0, flag === "u" ? {
36+
unicode: /* uFlag = */ true
37+
} : {
38+
unicodeSets: true
39+
});
3240
} catch {
3341
return false;
3442
}

0 commit comments

Comments
 (0)