Skip to content

Commit

Permalink
feat: no-empty-character-class support v flag (#17419)
Browse files Browse the repository at this point in the history
Reports nested character classes

Refs #17223
  • Loading branch information
mdjermanovic committed Jul 27, 2023
1 parent 853d32b commit ee68d1d
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 14 deletions.
20 changes: 20 additions & 0 deletions docs/src/rules/no-empty-character-class.md
Expand Up @@ -24,6 +24,23 @@ Examples of **incorrect** code for this rule:

/^abc[]/.test("abcdefg"); // false
"abcdefg".match(/^abc[]/); // null

/^abc[[]]/v.test("abcdefg"); // false
"abcdefg".match(/^abc[[]]/v); // null

/^abc[[]--[x]]/v.test("abcdefg"); // false
"abcdefg".match(/^abc[[]--[x]]/v); // null

/^abc[[d]&&[]]/v.test("abcdefg"); // false
"abcdefg".match(/^abc[[d]&&[]]/v); // null

const regex = /^abc[d[]]/v;
regex.test("abcdefg"); // true, the nested `[]` has no effect
"abcdefg".match(regex); // ["abcd"]
regex.test("abcefg"); // false, the nested `[]` has no effect
"abcefg".match(regex); // null
regex.test("abc"); // false, the nested `[]` has no effect
"abc".match(regex); // null
```

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

/^abc[a-z]/.test("abcdefg"); // true
"abcdefg".match(/^abc[a-z]/); // ["abcd"]

/^abc[^]/.test("abcdefg"); // true
"abcdefg".match(/^abc[^]/); // ["abcd"]
```

:::
Expand Down
45 changes: 33 additions & 12 deletions lib/rules/no-empty-character-class.js
Expand Up @@ -5,20 +5,18 @@

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -45,9 +43,32 @@ module.exports = {
create(context) {
return {
"Literal[regex]"(node) {
if (!regex.test(node.regex.pattern)) {
context.report({ node, messageId: "unexpected" });
const { pattern, flags } = node.regex;

if (!QUICK_TEST_REGEX.test(pattern)) {
return;
}

let regExpAST;

try {
regExpAST = parser.parsePattern(pattern, 0, pattern.length, {
unicode: flags.includes("u"),
unicodeSets: flags.includes("v")
});
} catch {

// Ignore regular expressions that regexpp cannot parse
return;
}

visitRegExpAST(regExpAST, {
onCharacterClassEnter(characterClass) {
if (!characterClass.negate && characterClass.elements.length === 0) {
context.report({ node, messageId: "unexpected" });
}
}
});
}
};

Expand Down
24 changes: 22 additions & 2 deletions tests/lib/rules/no-empty-character-class.js
Expand Up @@ -25,15 +25,26 @@ ruleTester.run("no-empty-character-class", rule, {
"var foo = /^abc/;",
"var foo = /[\\[]/;",
"var foo = /[\\]]/;",
"var foo = /\\[][\\]]/;",
"var foo = /[a-zA-Z\\[]/;",
"var foo = /[[]/;",
"var foo = /[\\[a-z[]]/;",
"var foo = /[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g;",
"var foo = /\\s*:\\s*/gim;",
"var foo = /[^]/;", // this rule allows negated empty character classes
"var foo = /\\[][^]/;",
{ code: "var foo = /[\\]]/uy;", parserOptions: { ecmaVersion: 6 } },
{ code: "var foo = /[\\]]/s;", parserOptions: { ecmaVersion: 2018 } },
{ code: "var foo = /[\\]]/d;", parserOptions: { ecmaVersion: 2022 } },
"var foo = /\\[]/"
"var foo = /\\[]/",
{ code: "var foo = /[[^]]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[[\\]]]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[[\\[]]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[a--b]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[a&&b]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[[a][b]]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[\\q{}]/v;", parserOptions: { ecmaVersion: 2024 } },
{ code: "var foo = /[[^]--\\p{ASCII}]/v;", parserOptions: { ecmaVersion: 2024 } }
],
invalid: [
{ code: "var foo = /^abc[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
Expand All @@ -43,6 +54,15 @@ ruleTester.run("no-empty-character-class", rule, {
{ code: "var foo = /[]]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /\\[[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /\\[\\[\\]a-z[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[]]/d;", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Literal" }] }
{ code: "var foo = /[]]/d;", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[(]\\u{0}*[]/u;", parserOptions: { ecmaVersion: 2015 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[[a][]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[a[[b[]c]]d]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[a--[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[[]--b]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[a&&[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] },
{ code: "var foo = /[[]&&b]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }
]
});

0 comments on commit ee68d1d

Please sign in to comment.