Skip to content

Commit 38ae956

Browse files
authored
feat: check Unicode code point escapes in no-control-regex (#15862)
Fixes #15809
1 parent 36287c0 commit 38ae956

3 files changed

Lines changed: 73 additions & 18 deletions

File tree

docs/src/rules/no-control-regex.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The following elements of regular expression patterns are considered possible er
2222

2323
* Hexadecimal character escapes from `\x00` to `\x1F`.
2424
* Unicode character escapes from `\u0000` to `\u001F`.
25+
* Unicode code point escapes from `\u{0}` to `\u{1F}`.
2526
* Unescaped raw characters from U+0000 to U+001F.
2627

2728
Control escapes such as `\t` and `\n` are allowed by this rule.
@@ -35,8 +36,9 @@ var pattern1 = /\x00/;
3536
var pattern2 = /\x0C/;
3637
var pattern3 = /\x1F/;
3738
var pattern4 = /\u000C/;
38-
var pattern5 = new RegExp("\x0C"); // raw U+000C character in the pattern
39-
var pattern6 = new RegExp("\\x0C"); // \x0C pattern
39+
var pattern5 = /\u{C}/u;
40+
var pattern6 = new RegExp("\x0C"); // raw U+000C character in the pattern
41+
var pattern7 = new RegExp("\\x0C"); // \x0C pattern
4042
```
4143

4244
Examples of **correct** code for this rule:
@@ -46,11 +48,12 @@ Examples of **correct** code for this rule:
4648

4749
var pattern1 = /\x20/;
4850
var pattern2 = /\u0020/;
49-
var pattern3 = /\t/;
50-
var pattern4 = /\n/;
51-
var pattern5 = new RegExp("\x20");
52-
var pattern6 = new RegExp("\\t");
53-
var pattern7 = new RegExp("\\n");
51+
var pattern3 = /\u{20}/u;
52+
var pattern4 = /\t/;
53+
var pattern5 = /\n/;
54+
var pattern6 = new RegExp("\x20");
55+
var pattern7 = new RegExp("\\t");
56+
var pattern8 = new RegExp("\\n");
5457
```
5558

5659
## Known Limitations

lib/rules/no-control-regex.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ const collector = new (class {
3030
}
3131
}
3232

33-
collectControlChars(regexpStr) {
33+
collectControlChars(regexpStr, flags) {
34+
const uFlag = typeof flags === "string" && flags.includes("u");
35+
3436
try {
3537
this._source = regexpStr;
36-
this._validator.validatePattern(regexpStr); // Call onCharacter hook
38+
this._validator.validatePattern(regexpStr, void 0, void 0, uFlag); // Call onCharacter hook
3739
} catch {
3840

3941
// Ignore syntax errors in RegExp.
@@ -68,32 +70,43 @@ module.exports = {
6870

6971
/**
7072
* Get the regex expression
71-
* @param {ASTNode} node node to evaluate
72-
* @returns {RegExp|null} Regex if found else null
73+
* @param {ASTNode} node `Literal` node to evaluate
74+
* @returns {{ pattern: string, flags: string | null } | null} Regex if found (the given node is either a regex literal
75+
* or a string literal that is the pattern argument of a RegExp constructor call). Otherwise `null`. If flags cannot be determined,
76+
* the `flags` property will be `null`.
7377
* @private
7478
*/
75-
function getRegExpPattern(node) {
79+
function getRegExp(node) {
7680
if (node.regex) {
77-
return node.regex.pattern;
81+
return node.regex;
7882
}
7983
if (typeof node.value === "string" &&
8084
(node.parent.type === "NewExpression" || node.parent.type === "CallExpression") &&
8185
node.parent.callee.type === "Identifier" &&
8286
node.parent.callee.name === "RegExp" &&
8387
node.parent.arguments[0] === node
8488
) {
85-
return node.value;
89+
const pattern = node.value;
90+
const flags =
91+
node.parent.arguments.length > 1 &&
92+
node.parent.arguments[1].type === "Literal" &&
93+
typeof node.parent.arguments[1].value === "string"
94+
? node.parent.arguments[1].value
95+
: null;
96+
97+
return { pattern, flags };
8698
}
8799

88100
return null;
89101
}
90102

91103
return {
92104
Literal(node) {
93-
const pattern = getRegExpPattern(node);
105+
const regExp = getRegExp(node);
94106

95-
if (pattern) {
96-
const controlCharacters = collector.collectControlChars(pattern);
107+
if (regExp) {
108+
const { pattern, flags } = regExp;
109+
const controlCharacters = collector.collectControlChars(pattern, flags);
97110

98111
if (controlCharacters.length > 0) {
99112
context.report({

tests/lib/rules/no-control-regex.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ ruleTester.run("no-control-regex", rule, {
2626
"var regex = RegExp('x1f')",
2727
"new RegExp('[')",
2828
"RegExp('[')",
29-
"new (function foo(){})('\\x1f')"
29+
"new (function foo(){})('\\x1f')",
30+
{ code: String.raw`/\u{20}/u`, parserOptions: { ecmaVersion: 2015 } },
31+
String.raw`/\u{1F}/`,
32+
String.raw`/\u{1F}/g`,
33+
String.raw`new RegExp("\\u{20}", "u")`,
34+
String.raw`new RegExp("\\u{1F}")`,
35+
String.raw`new RegExp("\\u{1F}", "g")`,
36+
String.raw`new RegExp("\\u{1F}", flags)` // when flags are unknown, this rule assumes there's no `u` flag
3037
],
3138
invalid: [
3239
{ code: String.raw`var regex = /\x1f/`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] },
@@ -46,6 +53,38 @@ ruleTester.run("no-control-regex", rule, {
4653
code: String.raw`var regex = /(?<\u{1d49c}>.)\x1f/`,
4754
parserOptions: { ecmaVersion: 2020 },
4855
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
56+
},
57+
{
58+
code: String.raw`new RegExp("\\u001F", flags)`,
59+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
60+
},
61+
{
62+
code: String.raw`/\u{1111}*\x1F/u`,
63+
parserOptions: { ecmaVersion: 2015 },
64+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
65+
},
66+
{
67+
code: String.raw`new RegExp("\\u{1111}*\\x1F", "u")`,
68+
parserOptions: { ecmaVersion: 2015 },
69+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
70+
},
71+
{
72+
code: String.raw`/\u{1F}/u`,
73+
parserOptions: { ecmaVersion: 2015 },
74+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
75+
},
76+
{
77+
code: String.raw`/\u{1F}/gui`,
78+
parserOptions: { ecmaVersion: 2015 },
79+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
80+
},
81+
{
82+
code: String.raw`new RegExp("\\u{1F}", "u")`,
83+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
84+
},
85+
{
86+
code: String.raw`new RegExp("\\u{1F}", "gui")`,
87+
errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
4988
}
5089
]
5190
});

0 commit comments

Comments
 (0)