Skip to content

Commit b6ab8b2

Browse files
feat: require-unicode-regexp add suggestions (#17007)
* feat: `require-unicode-regexp` add suggestions * Review fixups: invalid patterns; sequence expressions * I promise I know how CJS modules work * Handled non-string literals * Update lib/rules/require-unicode-regexp.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Don't concatenate + 'u' * Test the new cases * Touch up regular-expressions.js templating * Add myself as author to regular-expressions.js --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 4dd8d52 commit b6ab8b2

5 files changed

Lines changed: 297 additions & 51 deletions

File tree

lib/rules/no-misleading-character-class.js

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
"use strict";
55

66
const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils");
7-
const { RegExpValidator, RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
7+
const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
88
const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode");
99
const astUtils = require("./utils/ast-utils.js");
10+
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
1011

1112
//------------------------------------------------------------------------------
1213
// Helpers
1314
//------------------------------------------------------------------------------
1415

15-
const REGEXPP_LATEST_ECMA_VERSION = 2022;
16-
1716
/**
1817
* Iterate character sequences of a given nodes.
1918
*
@@ -185,38 +184,10 @@ module.exports = {
185184
}
186185
}
187186

188-
/**
189-
* Checks if the given regular expression pattern would be valid with the `u` flag.
190-
* @param {string} pattern The regular expression pattern to verify.
191-
* @returns {boolean} `true` if the pattern would be valid with the `u` flag.
192-
* `false` if the pattern would be invalid with the `u` flag or the configured
193-
* ecmaVersion doesn't support the `u` flag.
194-
*/
195-
function isValidWithUnicodeFlag(pattern) {
196-
const { ecmaVersion } = context.languageOptions;
197-
198-
// ecmaVersion <= 5 doesn't support the 'u' flag
199-
if (ecmaVersion <= 5) {
200-
return false;
201-
}
202-
203-
const validator = new RegExpValidator({
204-
ecmaVersion: Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION)
205-
});
206-
207-
try {
208-
validator.validatePattern(pattern, void 0, void 0, /* uFlag = */ true);
209-
} catch {
210-
return false;
211-
}
212-
213-
return true;
214-
}
215-
216187
return {
217188
"Literal[regex]"(node) {
218189
verify(node, node.regex.pattern, node.regex.flags, fixer => {
219-
if (!isValidWithUnicodeFlag(node.regex.pattern)) {
190+
if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)) {
220191
return null;
221192
}
222193

@@ -242,7 +213,7 @@ module.exports = {
242213
if (typeof pattern === "string") {
243214
verify(refNode, pattern, flags || "", fixer => {
244215

245-
if (!isValidWithUnicodeFlag(pattern)) {
216+
if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)) {
246217
return null;
247218
}
248219

lib/rules/prefer-regex-literals.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ const astUtils = require("./utils/ast-utils");
1313
const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("@eslint-community/eslint-utils");
1414
const { RegExpValidator, visitRegExpAST, RegExpParser } = require("@eslint-community/regexpp");
1515
const { canTokensBeAdjacent } = require("./utils/ast-utils");
16+
const { REGEXPP_LATEST_ECMA_VERSION } = require("./utils/regular-expressions");
1617

1718
//------------------------------------------------------------------------------
1819
// Helpers
1920
//------------------------------------------------------------------------------
2021

21-
const REGEXPP_LATEST_ECMA_VERSION = 2022;
22-
2322
/**
2423
* Determines whether the given node is a string literal.
2524
* @param {ASTNode} node Node to check.

lib/rules/require-unicode-regexp.js

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const {
1515
ReferenceTracker,
1616
getStringIfConstant
1717
} = require("@eslint-community/eslint-utils");
18+
const astUtils = require("./utils/ast-utils.js");
19+
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
1820

1921
//------------------------------------------------------------------------------
2022
// Rule Definition
@@ -31,7 +33,10 @@ module.exports = {
3133
url: "https://eslint.org/docs/rules/require-unicode-regexp"
3234
},
3335

36+
hasSuggestions: true,
37+
3438
messages: {
39+
addUFlag: "Add the 'u' flag.",
3540
requireUFlag: "Use the 'u' flag."
3641
},
3742

@@ -47,7 +52,20 @@ module.exports = {
4752
const flags = node.regex.flags || "";
4853

4954
if (!flags.includes("u")) {
50-
context.report({ node, messageId: "requireUFlag" });
55+
context.report({
56+
messageId: "requireUFlag",
57+
node,
58+
suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)
59+
? [
60+
{
61+
fix(fixer) {
62+
return fixer.insertTextAfter(node, "u");
63+
},
64+
messageId: "addUFlag"
65+
}
66+
]
67+
: null
68+
});
5169
}
5270
},
5371

@@ -59,11 +77,46 @@ module.exports = {
5977
};
6078

6179
for (const { node: refNode } of tracker.iterateGlobalReferences(trackMap)) {
62-
const flagsNode = refNode.arguments[1];
80+
const [patternNode, flagsNode] = refNode.arguments;
81+
const pattern = getStringIfConstant(patternNode, scope);
6382
const flags = getStringIfConstant(flagsNode, scope);
6483

6584
if (!flagsNode || (typeof flags === "string" && !flags.includes("u"))) {
66-
context.report({ node: refNode, messageId: "requireUFlag" });
85+
context.report({
86+
messageId: "requireUFlag",
87+
node: refNode,
88+
suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)
89+
? [
90+
{
91+
fix(fixer) {
92+
if (flagsNode) {
93+
if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") {
94+
const flagsNodeText = sourceCode.getText(flagsNode);
95+
96+
return fixer.replaceText(flagsNode, [
97+
flagsNodeText.slice(0, flagsNodeText.length - 1),
98+
flagsNodeText.slice(flagsNodeText.length - 1)
99+
].join("u"));
100+
}
101+
102+
// We intentionally don't suggest concatenating + "u" to non-literals
103+
return null;
104+
}
105+
106+
const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis
107+
108+
return fixer.insertTextAfter(
109+
penultimateToken,
110+
astUtils.isCommaToken(penultimateToken)
111+
? ' "u",'
112+
: ', "u"'
113+
);
114+
},
115+
messageId: "addUFlag"
116+
}
117+
]
118+
: null
119+
});
67120
}
68121
}
69122
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @fileoverview Common utils for regular expressions.
3+
* @author Josh Goldberg
4+
* @author Toru Nagashima
5+
*/
6+
7+
"use strict";
8+
9+
const { RegExpValidator } = require("@eslint-community/regexpp");
10+
11+
const REGEXPP_LATEST_ECMA_VERSION = 2022;
12+
13+
/**
14+
* Checks if the given regular expression pattern would be valid with the `u` flag.
15+
* @param {number} ecmaVersion ECMAScript version to parse in.
16+
* @param {string} pattern The regular expression pattern to verify.
17+
* @returns {boolean} `true` if the pattern would be valid with the `u` flag.
18+
* `false` if the pattern would be invalid with the `u` flag or the configured
19+
* ecmaVersion doesn't support the `u` flag.
20+
*/
21+
function isValidWithUnicodeFlag(ecmaVersion, pattern) {
22+
if (ecmaVersion <= 5) { // ecmaVersion <= 5 doesn't support the 'u' flag
23+
return false;
24+
}
25+
26+
const validator = new RegExpValidator({
27+
ecmaVersion: Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION)
28+
});
29+
30+
try {
31+
validator.validatePattern(pattern, void 0, void 0, /* uFlag = */ true);
32+
} catch {
33+
return false;
34+
}
35+
36+
return true;
37+
}
38+
39+
module.exports = {
40+
isValidWithUnicodeFlag,
41+
REGEXPP_LATEST_ECMA_VERSION
42+
};

0 commit comments

Comments
 (0)