diff --git a/docs/src/rules/no-empty-character-class.md b/docs/src/rules/no-empty-character-class.md index 984f1e18330..2ab0679b394 100644 --- a/docs/src/rules/no-empty-character-class.md +++ b/docs/src/rules/no-empty-character-class.md @@ -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 ``` ::: @@ -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"] ``` ::: diff --git a/lib/rules/no-empty-character-class.js b/lib/rules/no-empty-character-class.js index da29bbe9270..5c8410235bc 100644 --- a/lib/rules/no-empty-character-class.js +++ b/lib/rules/no-empty-character-class.js @@ -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 @@ -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" }); + } + } + }); } }; diff --git a/tests/lib/rules/no-empty-character-class.js b/tests/lib/rules/no-empty-character-class.js index fd4cef8ed77..81b66c4b300 100644 --- a/tests/lib/rules/no-empty-character-class.js +++ b/tests/lib/rules/no-empty-character-class.js @@ -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" }] }, @@ -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" }] } ] });