Skip to content

Commit ee68d1d

Browse files
authored
feat: no-empty-character-class support v flag (#17419)
Reports nested character classes Refs #17223
1 parent 853d32b commit ee68d1d

File tree

3 files changed

+75
-14
lines changed

3 files changed

+75
-14
lines changed

docs/src/rules/no-empty-character-class.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ Examples of **incorrect** code for this rule:
2424

2525
/^abc[]/.test("abcdefg"); // false
2626
"abcdefg".match(/^abc[]/); // null
27+
28+
/^abc[[]]/v.test("abcdefg"); // false
29+
"abcdefg".match(/^abc[[]]/v); // null
30+
31+
/^abc[[]--[x]]/v.test("abcdefg"); // false
32+
"abcdefg".match(/^abc[[]--[x]]/v); // null
33+
34+
/^abc[[d]&&[]]/v.test("abcdefg"); // false
35+
"abcdefg".match(/^abc[[d]&&[]]/v); // null
36+
37+
const regex = /^abc[d[]]/v;
38+
regex.test("abcdefg"); // true, the nested `[]` has no effect
39+
"abcdefg".match(regex); // ["abcd"]
40+
regex.test("abcefg"); // false, the nested `[]` has no effect
41+
"abcefg".match(regex); // null
42+
regex.test("abc"); // false, the nested `[]` has no effect
43+
"abc".match(regex); // null
2744
```
2845

2946
:::
@@ -40,6 +57,9 @@ Examples of **correct** code for this rule:
4057

4158
/^abc[a-z]/.test("abcdefg"); // true
4259
"abcdefg".match(/^abc[a-z]/); // ["abcd"]
60+
61+
/^abc[^]/.test("abcdefg"); // true
62+
"abcdefg".match(/^abc[^]/); // ["abcd"]
4363
```
4464

4565
:::

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

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@
55

66
"use strict";
77

8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
13+
814
//------------------------------------------------------------------------------
915
// Helpers
1016
//------------------------------------------------------------------------------
1117

12-
/*
13-
* plain-English description of the following regexp:
14-
* 0. `^` fix the match at the beginning of the string
15-
* 1. `([^\\[]|\\.|\[([^\\\]]|\\.)+\])*`: regexp contents; 0 or more of the following
16-
* 1.0. `[^\\[]`: any character that's not a `\` or a `[` (anything but escape sequences and character classes)
17-
* 1.1. `\\.`: an escape sequence
18-
* 1.2. `\[([^\\\]]|\\.)+\]`: a character class that isn't empty
19-
* 2. `$`: fix the match at the end of the string
20-
*/
21-
const regex = /^([^\\[]|\\.|\[([^\\\]]|\\.)+\])*$/u;
18+
const parser = new RegExpParser();
19+
const QUICK_TEST_REGEX = /\[\]/u;
2220

2321
//------------------------------------------------------------------------------
2422
// Rule Definition
@@ -45,9 +43,32 @@ module.exports = {
4543
create(context) {
4644
return {
4745
"Literal[regex]"(node) {
48-
if (!regex.test(node.regex.pattern)) {
49-
context.report({ node, messageId: "unexpected" });
46+
const { pattern, flags } = node.regex;
47+
48+
if (!QUICK_TEST_REGEX.test(pattern)) {
49+
return;
5050
}
51+
52+
let regExpAST;
53+
54+
try {
55+
regExpAST = parser.parsePattern(pattern, 0, pattern.length, {
56+
unicode: flags.includes("u"),
57+
unicodeSets: flags.includes("v")
58+
});
59+
} catch {
60+
61+
// Ignore regular expressions that regexpp cannot parse
62+
return;
63+
}
64+
65+
visitRegExpAST(regExpAST, {
66+
onCharacterClassEnter(characterClass) {
67+
if (!characterClass.negate && characterClass.elements.length === 0) {
68+
context.report({ node, messageId: "unexpected" });
69+
}
70+
}
71+
});
5172
}
5273
};
5374

tests/lib/rules/no-empty-character-class.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,26 @@ ruleTester.run("no-empty-character-class", rule, {
2525
"var foo = /^abc/;",
2626
"var foo = /[\\[]/;",
2727
"var foo = /[\\]]/;",
28+
"var foo = /\\[][\\]]/;",
2829
"var foo = /[a-zA-Z\\[]/;",
2930
"var foo = /[[]/;",
3031
"var foo = /[\\[a-z[]]/;",
3132
"var foo = /[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g;",
3233
"var foo = /\\s*:\\s*/gim;",
34+
"var foo = /[^]/;", // this rule allows negated empty character classes
35+
"var foo = /\\[][^]/;",
3336
{ code: "var foo = /[\\]]/uy;", parserOptions: { ecmaVersion: 6 } },
3437
{ code: "var foo = /[\\]]/s;", parserOptions: { ecmaVersion: 2018 } },
3538
{ code: "var foo = /[\\]]/d;", parserOptions: { ecmaVersion: 2022 } },
36-
"var foo = /\\[]/"
39+
"var foo = /\\[]/",
40+
{ code: "var foo = /[[^]]/v;", parserOptions: { ecmaVersion: 2024 } },
41+
{ code: "var foo = /[[\\]]]/v;", parserOptions: { ecmaVersion: 2024 } },
42+
{ code: "var foo = /[[\\[]]/v;", parserOptions: { ecmaVersion: 2024 } },
43+
{ code: "var foo = /[a--b]/v;", parserOptions: { ecmaVersion: 2024 } },
44+
{ code: "var foo = /[a&&b]/v;", parserOptions: { ecmaVersion: 2024 } },
45+
{ code: "var foo = /[[a][b]]/v;", parserOptions: { ecmaVersion: 2024 } },
46+
{ code: "var foo = /[\\q{}]/v;", parserOptions: { ecmaVersion: 2024 } },
47+
{ code: "var foo = /[[^]--\\p{ASCII}]/v;", parserOptions: { ecmaVersion: 2024 } }
3748
],
3849
invalid: [
3950
{ code: "var foo = /^abc[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
@@ -43,6 +54,15 @@ ruleTester.run("no-empty-character-class", rule, {
4354
{ code: "var foo = /[]]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
4455
{ code: "var foo = /\\[[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
4556
{ code: "var foo = /\\[\\[\\]a-z[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
46-
{ code: "var foo = /[]]/d;", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Literal" }] }
57+
{ code: "var foo = /[]]/d;", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
58+
{ code: "var foo = /[(]\\u{0}*[]/u;", parserOptions: { ecmaVersion: 2015 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
59+
{ code: "var foo = /[]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
60+
{ code: "var foo = /[[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
61+
{ code: "var foo = /[[a][]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
62+
{ code: "var foo = /[a[[b[]c]]d]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
63+
{ code: "var foo = /[a--[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
64+
{ code: "var foo = /[[]--b]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
65+
{ code: "var foo = /[a&&[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
66+
{ code: "var foo = /[[]&&b]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }
4767
]
4868
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy