From 03c146cd2105e09f2d3e8c8ca49d8aacd2ad8a30 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 15:32:01 +0900 Subject: [PATCH 01/13] feat: implement RegExp literal --- lib/rules/require-unicode-regexp.js | 60 ++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index f748753a2d1..3f5ef2de692 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -28,7 +28,7 @@ module.exports = { type: "suggestion", docs: { - description: "Enforce the use of `u` flag on RegExp", + description: "Enforce the use of `u` and `v` flag on RegExp", recommended: false, url: "https://eslint.org/docs/latest/rules/require-unicode-regexp" }, @@ -37,23 +37,65 @@ module.exports = { messages: { addUFlag: "Add the 'u' flag.", - requireUFlag: "Use the 'u' flag." + requireUFlag: "Use the 'u' flag.", + addVFlag: "Add the 'v' flag.", + requireVFlag: "Use the 'v' flag." }, - schema: [] + schema: [{ + type: "object", + properties: { + requiredUnicodeFlag: { + type: "string", + enum: ["u", "v"] + } + }, + additionalProperties: false, + description: "The required flag. If this is not specified, this rule allows both 'u' and 'v' flags." + }] }, create(context) { const sourceCode = context.sourceCode; + const config = context.options[0] || {}; + const requiredUnicodeFlag = config.requiredUnicodeFlag || null; + + const isRequiredUFlagOnly = requiredUnicodeFlag === "u"; + const isRequiredVFlagOnly = requiredUnicodeFlag === "v"; + + /** + * Checks whether or not the given flags include the required flags. + * @param {string} flags RegExp flags + * @returns {{ shouldReportForUFlag: boolean, shouldReportForVFlag: boolean }} `true` if the given flags include the required flags. + */ + function shouldReport(flags) { + const includesUFlag = flags.includes("u"); + const includesVFlag = flags.includes("v"); + + const shouldReportForUFlag = + (isRequiredUFlagOnly && !includesUFlag) || + (!isRequiredUFlagOnly && !isRequiredVFlagOnly && !includesUFlag && !includesVFlag); + const shouldReportForVFlag = isRequiredVFlagOnly && !includesVFlag; + + return { shouldReportForUFlag, shouldReportForVFlag }; + } + return { "Literal[regex]"(node) { const flags = node.regex.flags || ""; - if (!flags.includes("u")) { + const { shouldReportForUFlag, shouldReportForVFlag } = shouldReport(flags); + + /** + * Reports a RegExp literal without unicode flag. + * @param {boolean} forVFlag `true` if the required flag is 'v'. + * @returns {void} + */ + function reportRegExpLiteral(forVFlag) { context.report({ - messageId: "requireUFlag", + messageId: forVFlag ? "requireVFlag" : "requireUFlag", node, suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern) ? [ @@ -61,12 +103,18 @@ module.exports = { fix(fixer) { return fixer.insertTextAfter(node, "u"); }, - messageId: "addUFlag" + messageId: forVFlag ? "addVFlag" : "addUFlag" } ] : null }); } + + if (shouldReportForUFlag) { + reportRegExpLiteral(/* forVFlag */ false); + } else if (shouldReportForVFlag) { + reportRegExpLiteral(/* forVFlat */ true); + } }, Program(node) { From 456055e10c952c87ddb1edcbd9ee2faad48c1e2f Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 16:15:20 +0900 Subject: [PATCH 02/13] test: add tests for RegExp literal --- tests/lib/rules/require-unicode-regexp.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index a75f6863168..3743bcd86a8 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -45,7 +45,23 @@ ruleTester.run("require-unicode-regexp", rule, { { code: "const flags = 'u'; new globalThis.RegExp('', flags)", env: { es2020: true } }, { code: "const flags = 'g'; new globalThis.RegExp('', flags + 'u')", env: { es2020: true } }, { code: "const flags = 'gimu'; new globalThis.RegExp('foo', flags[3])", env: { es2020: true } }, - { code: "class C { #RegExp; foo() { new globalThis.#RegExp('foo') } }", parserOptions: { ecmaVersion: 2022 }, env: { es2020: true } } + { code: "class C { #RegExp; foo() { new globalThis.#RegExp('foo') } }", parserOptions: { ecmaVersion: 2022 }, env: { es2020: true } }, + + // for v flag + { code: "/foo/v", parserOptions: { ecmaVersion: 2024 } }, + { code: "/foo/gimuy", parserOptions: { ecmaVersion: 2024 } } + + /* + * { code: "RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, + * { code: "RegExp('', `v`)", parserOptions: { ecmaVersion: 2024 } }, + * { code: "new RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, + * { code: "RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, + * { code: "RegExp('', `gimvy`)", parserOptions: { ecmaVersion: 2024 } }, + * { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, + * { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, + * { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, + * { code: "const flags = 'gimv'; new RegExp('foo', flags[3])", parserOptions: { ecmaVersion: 2024 } } + */ ], invalid: [ { From cf2c2311bec23875fc8008b52da2f453a6a8d99e Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 16:36:11 +0900 Subject: [PATCH 03/13] feat: implement for RegExp constructor --- lib/rules/require-unicode-regexp.js | 76 ++++++++++++++++------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index 3f5ef2de692..be2ad35f7bb 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -133,42 +133,50 @@ module.exports = { const pattern = getStringIfConstant(patternNode, scope); const flags = getStringIfConstant(flagsNode, scope); - if (!flagsNode || (typeof flags === "string" && !flags.includes("u"))) { - context.report({ - messageId: "requireUFlag", - node: refNode, - suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern) - ? [ - { - fix(fixer) { - if (flagsNode) { - if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") { - const flagsNodeText = sourceCode.getText(flagsNode); - - return fixer.replaceText(flagsNode, [ - flagsNodeText.slice(0, flagsNodeText.length - 1), - flagsNodeText.slice(flagsNodeText.length - 1) - ].join("u")); + if (!flagsNode || (typeof flags === "string")) { + const { shouldReportForUFlag, shouldReportForVFlag } = shouldReport(flags || ""); + + if (shouldReportForUFlag || shouldReportForVFlag) { + const requireMessageId = shouldReportForVFlag ? "requireVFlag" : "requireUFlag"; + const addMessageId = shouldReportForVFlag ? "addVFlag" : "addUFlag"; + const flag = shouldReportForVFlag ? "v" : "u"; + + context.report({ + messageId: requireMessageId, + node: refNode, + suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern) + ? [ + { + fix(fixer) { + if (flagsNode) { + if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") { + const flagsNodeText = sourceCode.getText(flagsNode); + + return fixer.replaceText(flagsNode, [ + flagsNodeText.slice(0, flagsNodeText.length - 1), + flagsNodeText.slice(flagsNodeText.length - 1) + ].join(flag)); + } + + // We intentionally don't suggest concatenating + "u" to non-literals + return null; } - // We intentionally don't suggest concatenating + "u" to non-literals - return null; - } - - const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis - - return fixer.insertTextAfter( - penultimateToken, - astUtils.isCommaToken(penultimateToken) - ? ' "u",' - : ', "u"' - ); - }, - messageId: "addUFlag" - } - ] - : null - }); + const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis + + return fixer.insertTextAfter( + penultimateToken, + astUtils.isCommaToken(penultimateToken) + ? ` "${flag}", ` + : `, "${flag}"` + ); + }, + messageId: addMessageId + } + ] + : null + }); + } } } } From 6019a2208e27c1201582c3406aac76d18bc72e41 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 16:36:27 +0900 Subject: [PATCH 04/13] test: add tests for RegExp constructor --- tests/lib/rules/require-unicode-regexp.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index 3743bcd86a8..4fcf84ed7e2 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -49,19 +49,16 @@ ruleTester.run("require-unicode-regexp", rule, { // for v flag { code: "/foo/v", parserOptions: { ecmaVersion: 2024 } }, - { code: "/foo/gimuy", parserOptions: { ecmaVersion: 2024 } } - - /* - * { code: "RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, - * { code: "RegExp('', `v`)", parserOptions: { ecmaVersion: 2024 } }, - * { code: "new RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, - * { code: "RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, - * { code: "RegExp('', `gimvy`)", parserOptions: { ecmaVersion: 2024 } }, - * { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, - * { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, - * { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, - * { code: "const flags = 'gimv'; new RegExp('foo', flags[3])", parserOptions: { ecmaVersion: 2024 } } - */ + { code: "/foo/gimuy", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', `v`)", parserOptions: { ecmaVersion: 2024 } }, + { code: "new RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', `gimvy`)", parserOptions: { ecmaVersion: 2024 } }, + { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, + { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, + { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, + { code: "const flags = 'gimv'; new RegExp('foo', flags[3])", parserOptions: { ecmaVersion: 2024 } } ], invalid: [ { From 9970ae2f12696701f886ae7e5faa473564381421 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 16:55:08 +0900 Subject: [PATCH 05/13] test: add invalid tests --- lib/rules/require-unicode-regexp.js | 2 +- tests/lib/rules/require-unicode-regexp.js | 55 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index be2ad35f7bb..bba6d1a335b 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -101,7 +101,7 @@ module.exports = { ? [ { fix(fixer) { - return fixer.insertTextAfter(node, "u"); + return fixer.insertTextAfter(node, forVFlag ? "v" : "u"); }, messageId: forVFlag ? "addVFlag" : "addUFlag" } diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index 4fcf84ed7e2..2d9af4831f1 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -301,6 +301,61 @@ ruleTester.run("require-unicode-regexp", rule, { } ] }] + }, + + { + code: "/test/", + options: [{ requiredUnicodeFlag: "v" }], + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + messageId: "requireVFlag", + suggestions: [ + { + messageId: "addVFlag", + output: "/test/v" + } + ] + }] + }, + { + code: "/test/", + options: [{ requiredUnicodeFlag: "u" }], + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + messageId: "requireUFlag", + suggestions: [ + { + messageId: "addUFlag", + output: "/test/u" + } + ] + }] + }, + { + code: "/foo/gimy", + options: [{ requiredUnicodeFlag: "v" }], + errors: [{ + messageId: "requireVFlag", + suggestions: [ + { + messageId: "addVFlag", + output: "/foo/gimyv" + } + ] + }] + }, + { + code: "/foo/gimy", + options: [{ requiredUnicodeFlag: "u" }], + errors: [{ + messageId: "requireUFlag", + suggestions: [ + { + messageId: "addUFlag", + output: "/foo/gimyu" + } + ] + }] } ] }); From 5a39eee9cd47f9b477f305a42e614fdbd06230b5 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 17:28:24 +0900 Subject: [PATCH 06/13] feat: implement replacement for u and v --- lib/rules/require-unicode-regexp.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index bba6d1a335b..8b155eeb7bb 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -101,6 +101,24 @@ module.exports = { ? [ { fix(fixer) { + + // /test/gimuy -> /test/gimvy, /test/gimvy -> /test/gimuy + if ((forVFlag && flags.includes("u")) || (!forVFlag && flags.includes("v"))) { + const searchValue = forVFlag ? "u" : "v"; + const replaceValue = forVFlag ? "v" : "u"; + + const newFlags = flags.replace(searchValue, replaceValue); + + /** + * /test/gimyu + * ^^^^^ + */ + const rangeForFlags = [node.range[1] - flags.length, node.range[1]]; + + return fixer.replaceTextRange(rangeForFlags, newFlags); + } + + // /test/g -> /test/gu return fixer.insertTextAfter(node, forVFlag ? "v" : "u"); }, messageId: forVFlag ? "addVFlag" : "addUFlag" From b310c1a8de2d4ce4d03dd773b86dbefbb142bd80 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 17:28:35 +0900 Subject: [PATCH 07/13] test: add tests --- tests/lib/rules/require-unicode-regexp.js | 79 ++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index 2d9af4831f1..27c188233e7 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -58,7 +58,28 @@ ruleTester.run("require-unicode-regexp", rule, { { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, - { code: "const flags = 'gimv'; new RegExp('foo', flags[3])", parserOptions: { ecmaVersion: 2024 } } + { code: "const flags = 'gimv'; new RegExp('foo', flags[4])", parserOptions: { ecmaVersion: 2024 } }, + + { + code: "/foo/u", + options: [{ requiredUnicodeFlag: "u" }], + parserOptions: { ecmaVersion: 2024 } + }, + { + code: "/foo/v", + options: [{ requiredUnicodeFlag: "v" }], + parserOptions: { ecmaVersion: 2024 } + }, + { + code: "/foo/gimuy", + options: [{ requiredUnicodeFlag: "u" }], + parserOptions: { ecmaVersion: 2024 } + }, + { + code: "/foo/gimvy", + options: [{ requiredUnicodeFlag: "v" }], + parserOptions: { ecmaVersion: 2024 } + } ], invalid: [ { @@ -317,6 +338,62 @@ ruleTester.run("require-unicode-regexp", rule, { ] }] }, + { + code: "/test/gimyu", + options: [{ requiredUnicodeFlag: "v" }], + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + messageId: "requireVFlag", + suggestions: [ + { + messageId: "addVFlag", + output: "/test/gimyv" + } + ] + }] + }, + { + code: "/test/gimyv", + options: [{ requiredUnicodeFlag: "u" }], + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + messageId: "requireUFlag", + suggestions: [ + { + messageId: "addUFlag", + output: "/test/gimyu" + } + ] + }] + }, + { + code: "/test/gimuy", + options: [{ requiredUnicodeFlag: "v" }], + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + messageId: "requireVFlag", + suggestions: [ + { + messageId: "addVFlag", + output: "/test/gimvy" + } + ] + }] + }, + { + code: "/test/gimvy", + options: [{ requiredUnicodeFlag: "u" }], + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + messageId: "requireUFlag", + suggestions: [ + { + messageId: "addUFlag", + output: "/test/gimuy" + } + ] + }] + }, { code: "/test/", options: [{ requiredUnicodeFlag: "u" }], From 21ca65931c4acc274218e04727de362ed5446b71 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 18:03:18 +0900 Subject: [PATCH 08/13] docs: update docs for `v` flag --- docs/src/rules/require-unicode-regexp.md | 127 ++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/docs/src/rules/require-unicode-regexp.md b/docs/src/rules/require-unicode-regexp.md index f3277066a5b..1b7ab2c1a9d 100644 --- a/docs/src/rules/require-unicode-regexp.md +++ b/docs/src/rules/require-unicode-regexp.md @@ -21,7 +21,37 @@ RegExp `u` flag has two effects: The `u` flag disables the recovering logic Annex B defined. As a result, you can find errors early. This is similar to [the strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode). -Therefore, the `u` flag lets us work better with regular expressions. +The RegExp `v` flag, introduced in ECMAScript 2024, is a superset of the `u` flag, and offers two more features: + +1. **Unicode properties of strings** + + With the Unicode property escape, you can use properties of strings. + + ```js + const re = /^\p{RGI_Emoji}$/v; + + // Match an emoji that consists of just 1 code point: + re.test('⚽'); // '\u26BD' + // → true ✅ + + // Match an emoji that consists of multiple code points: + re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F' + // → true ✅ + ``` + +2. **Set notation** + + It allows for set operations between character classes. + + ```js + const re = /[\p{White_Space}&&\p{ASCII}]/v; + re.test('\n'); // → true + re.test('\u2028'); // → false + ``` + +Please see and for more details. + +Therefore, the `u` and `v` flag lets us work better with regular expressions. ## Rule Details @@ -54,6 +84,101 @@ const b = /bbb/giu const c = new RegExp("ccc", "u") const d = new RegExp("ddd", "giu") +const e = /aaa/v +const f = /bbb/giv +const g = new RegExp("ccc", "v") +const h = new RegExp("ddd", "giv") + +// This rule ignores RegExp calls if the flags could not be evaluated to a static value. +function f(flags) { + return new RegExp("eee", flags) +} +``` + +::: + +## Options + +This rule has one object option. + +### `requiredUnicodeFlag` + +This option can be set to `"u"` or `"v"`. By default, nothing is set. + +#### `u` + +Examples of **incorrect** code for this rule with `requiredUnicodeFlag` is `u`: + +::: incorrect + +```js +/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "u" }] */ + +const a = /aaa/ +const b = /bbb/gi +const c = new RegExp("ccc") +const d = new RegExp("ddd", "gi") +const e = /aaa/v +const f = /bbb/giv +const g = new RegExp("ccc", "v") +const h = new RegExp("ddd", "giv") +``` + +::: + +Examples of **correct** code for this rule: + +::: correct + +```js +/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "u" }] */ + +const a = /aaa/u +const b = /bbb/giu +const c = new RegExp("ccc", "u") +const d = new RegExp("ddd", "giu") + +// This rule ignores RegExp calls if the flags could not be evaluated to a static value. +function f(flags) { + return new RegExp("eee", flags) +} +``` + +::: + +#### `v` + +Examples of **incorrect** code for this rule with `requiredUnicodeFlag` is `v`: + +::: incorrect + +```js +/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "v" }] */ + +const a = /aaa/ +const b = /bbb/gi +const c = new RegExp("ccc") +const d = new RegExp("ddd", "gi") +const e = /aaa/u +const f = /bbb/giu +const g = new RegExp("ccc", "u") +const h = new RegExp("ddd", "giu") +``` + +::: + +Examples of **correct** code for this rule: + +::: correct + +```js +/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "v" }] */ + +const a = /aaa/v +const b = /bbb/giv +const c = new RegExp("ccc", "v") +const d = new RegExp("ddd", "giv") + // This rule ignores RegExp calls if the flags could not be evaluated to a static value. function f(flags) { return new RegExp("eee", flags) From c510badef09fe662115e9594df4f6a5bcf226ebd Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sat, 22 Jul 2023 22:23:05 +0900 Subject: [PATCH 09/13] fix: address review --- docs/src/rules/require-unicode-regexp.md | 90 ------------- lib/rules/require-unicode-regexp.js | 154 ++++++---------------- tests/lib/rules/require-unicode-regexp.js | 134 +------------------ 3 files changed, 41 insertions(+), 337 deletions(-) diff --git a/docs/src/rules/require-unicode-regexp.md b/docs/src/rules/require-unicode-regexp.md index 1b7ab2c1a9d..325da84925d 100644 --- a/docs/src/rules/require-unicode-regexp.md +++ b/docs/src/rules/require-unicode-regexp.md @@ -97,96 +97,6 @@ function f(flags) { ::: -## Options - -This rule has one object option. - -### `requiredUnicodeFlag` - -This option can be set to `"u"` or `"v"`. By default, nothing is set. - -#### `u` - -Examples of **incorrect** code for this rule with `requiredUnicodeFlag` is `u`: - -::: incorrect - -```js -/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "u" }] */ - -const a = /aaa/ -const b = /bbb/gi -const c = new RegExp("ccc") -const d = new RegExp("ddd", "gi") -const e = /aaa/v -const f = /bbb/giv -const g = new RegExp("ccc", "v") -const h = new RegExp("ddd", "giv") -``` - -::: - -Examples of **correct** code for this rule: - -::: correct - -```js -/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "u" }] */ - -const a = /aaa/u -const b = /bbb/giu -const c = new RegExp("ccc", "u") -const d = new RegExp("ddd", "giu") - -// This rule ignores RegExp calls if the flags could not be evaluated to a static value. -function f(flags) { - return new RegExp("eee", flags) -} -``` - -::: - -#### `v` - -Examples of **incorrect** code for this rule with `requiredUnicodeFlag` is `v`: - -::: incorrect - -```js -/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "v" }] */ - -const a = /aaa/ -const b = /bbb/gi -const c = new RegExp("ccc") -const d = new RegExp("ddd", "gi") -const e = /aaa/u -const f = /bbb/giu -const g = new RegExp("ccc", "u") -const h = new RegExp("ddd", "giu") -``` - -::: - -Examples of **correct** code for this rule: - -::: correct - -```js -/*eslint require-unicode-regexp: ["error", { "requiredUnicodeFlag": "v" }] */ - -const a = /aaa/v -const b = /bbb/giv -const c = new RegExp("ccc", "v") -const d = new RegExp("ddd", "giv") - -// This rule ignores RegExp calls if the flags could not be evaluated to a static value. -function f(flags) { - return new RegExp("eee", flags) -} -``` - -::: - ## When Not To Use It If you don't want to notify regular expressions with no `u` flag, then it's safe to disable this rule. diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index 8b155eeb7bb..18fbec2ba46 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -37,102 +37,36 @@ module.exports = { messages: { addUFlag: "Add the 'u' flag.", - requireUFlag: "Use the 'u' flag.", - addVFlag: "Add the 'v' flag.", - requireVFlag: "Use the 'v' flag." + requireUFlag: "Use the 'u' flag." }, - schema: [{ - type: "object", - properties: { - requiredUnicodeFlag: { - type: "string", - enum: ["u", "v"] - } - }, - additionalProperties: false, - description: "The required flag. If this is not specified, this rule allows both 'u' and 'v' flags." - }] + schema: [] }, create(context) { const sourceCode = context.sourceCode; - const config = context.options[0] || {}; - const requiredUnicodeFlag = config.requiredUnicodeFlag || null; - - const isRequiredUFlagOnly = requiredUnicodeFlag === "u"; - const isRequiredVFlagOnly = requiredUnicodeFlag === "v"; - - /** - * Checks whether or not the given flags include the required flags. - * @param {string} flags RegExp flags - * @returns {{ shouldReportForUFlag: boolean, shouldReportForVFlag: boolean }} `true` if the given flags include the required flags. - */ - function shouldReport(flags) { - const includesUFlag = flags.includes("u"); - const includesVFlag = flags.includes("v"); - - const shouldReportForUFlag = - (isRequiredUFlagOnly && !includesUFlag) || - (!isRequiredUFlagOnly && !isRequiredVFlagOnly && !includesUFlag && !includesVFlag); - const shouldReportForVFlag = isRequiredVFlagOnly && !includesVFlag; - - return { shouldReportForUFlag, shouldReportForVFlag }; - } - return { "Literal[regex]"(node) { const flags = node.regex.flags || ""; - const { shouldReportForUFlag, shouldReportForVFlag } = shouldReport(flags); - - /** - * Reports a RegExp literal without unicode flag. - * @param {boolean} forVFlag `true` if the required flag is 'v'. - * @returns {void} - */ - function reportRegExpLiteral(forVFlag) { + if (!flags.includes("u") && !flags.includes("v")) { context.report({ - messageId: forVFlag ? "requireVFlag" : "requireUFlag", + messageId: "requireUFlag", node, suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern) ? [ { fix(fixer) { - - // /test/gimuy -> /test/gimvy, /test/gimvy -> /test/gimuy - if ((forVFlag && flags.includes("u")) || (!forVFlag && flags.includes("v"))) { - const searchValue = forVFlag ? "u" : "v"; - const replaceValue = forVFlag ? "v" : "u"; - - const newFlags = flags.replace(searchValue, replaceValue); - - /** - * /test/gimyu - * ^^^^^ - */ - const rangeForFlags = [node.range[1] - flags.length, node.range[1]]; - - return fixer.replaceTextRange(rangeForFlags, newFlags); - } - - // /test/g -> /test/gu - return fixer.insertTextAfter(node, forVFlag ? "v" : "u"); + return fixer.insertTextAfter(node, "u"); }, - messageId: forVFlag ? "addVFlag" : "addUFlag" + messageId: "addUFlag" } ] : null }); } - - if (shouldReportForUFlag) { - reportRegExpLiteral(/* forVFlag */ false); - } else if (shouldReportForVFlag) { - reportRegExpLiteral(/* forVFlat */ true); - } }, Program(node) { @@ -151,50 +85,42 @@ module.exports = { const pattern = getStringIfConstant(patternNode, scope); const flags = getStringIfConstant(flagsNode, scope); - if (!flagsNode || (typeof flags === "string")) { - const { shouldReportForUFlag, shouldReportForVFlag } = shouldReport(flags || ""); - - if (shouldReportForUFlag || shouldReportForVFlag) { - const requireMessageId = shouldReportForVFlag ? "requireVFlag" : "requireUFlag"; - const addMessageId = shouldReportForVFlag ? "addVFlag" : "addUFlag"; - const flag = shouldReportForVFlag ? "v" : "u"; - - context.report({ - messageId: requireMessageId, - node: refNode, - suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern) - ? [ - { - fix(fixer) { - if (flagsNode) { - if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") { - const flagsNodeText = sourceCode.getText(flagsNode); - - return fixer.replaceText(flagsNode, [ - flagsNodeText.slice(0, flagsNodeText.length - 1), - flagsNodeText.slice(flagsNodeText.length - 1) - ].join(flag)); - } - - // We intentionally don't suggest concatenating + "u" to non-literals - return null; + if (!flagsNode || (typeof flags === "string" && !flags.includes("u") && !flags.includes("v"))) { + context.report({ + messageId: "requireUFlag", + node: refNode, + suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern) + ? [ + { + fix(fixer) { + if (flagsNode) { + if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") { + const flagsNodeText = sourceCode.getText(flagsNode); + + return fixer.replaceText(flagsNode, [ + flagsNodeText.slice(0, flagsNodeText.length - 1), + flagsNodeText.slice(flagsNodeText.length - 1) + ].join("u")); } - const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis - - return fixer.insertTextAfter( - penultimateToken, - astUtils.isCommaToken(penultimateToken) - ? ` "${flag}", ` - : `, "${flag}"` - ); - }, - messageId: addMessageId - } - ] - : null - }); - } + // We intentionally don't suggest concatenating + "u" to non-literals + return null; + } + + const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis + + return fixer.insertTextAfter( + penultimateToken, + astUtils.isCommaToken(penultimateToken) + ? ' "u",' + : ', "u"' + ); + }, + messageId: "addUFlag" + } + ] + : null + }); } } } diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index 27c188233e7..41187f2881b 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -58,28 +58,7 @@ ruleTester.run("require-unicode-regexp", rule, { { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, - { code: "const flags = 'gimv'; new RegExp('foo', flags[4])", parserOptions: { ecmaVersion: 2024 } }, - - { - code: "/foo/u", - options: [{ requiredUnicodeFlag: "u" }], - parserOptions: { ecmaVersion: 2024 } - }, - { - code: "/foo/v", - options: [{ requiredUnicodeFlag: "v" }], - parserOptions: { ecmaVersion: 2024 } - }, - { - code: "/foo/gimuy", - options: [{ requiredUnicodeFlag: "u" }], - parserOptions: { ecmaVersion: 2024 } - }, - { - code: "/foo/gimvy", - options: [{ requiredUnicodeFlag: "v" }], - parserOptions: { ecmaVersion: 2024 } - } + { code: "const flags = 'gimv'; new RegExp('foo', flags[4])", parserOptions: { ecmaVersion: 2024 } } ], invalid: [ { @@ -322,117 +301,6 @@ ruleTester.run("require-unicode-regexp", rule, { } ] }] - }, - - { - code: "/test/", - options: [{ requiredUnicodeFlag: "v" }], - parserOptions: { ecmaVersion: 2024 }, - errors: [{ - messageId: "requireVFlag", - suggestions: [ - { - messageId: "addVFlag", - output: "/test/v" - } - ] - }] - }, - { - code: "/test/gimyu", - options: [{ requiredUnicodeFlag: "v" }], - parserOptions: { ecmaVersion: 2024 }, - errors: [{ - messageId: "requireVFlag", - suggestions: [ - { - messageId: "addVFlag", - output: "/test/gimyv" - } - ] - }] - }, - { - code: "/test/gimyv", - options: [{ requiredUnicodeFlag: "u" }], - parserOptions: { ecmaVersion: 2024 }, - errors: [{ - messageId: "requireUFlag", - suggestions: [ - { - messageId: "addUFlag", - output: "/test/gimyu" - } - ] - }] - }, - { - code: "/test/gimuy", - options: [{ requiredUnicodeFlag: "v" }], - parserOptions: { ecmaVersion: 2024 }, - errors: [{ - messageId: "requireVFlag", - suggestions: [ - { - messageId: "addVFlag", - output: "/test/gimvy" - } - ] - }] - }, - { - code: "/test/gimvy", - options: [{ requiredUnicodeFlag: "u" }], - parserOptions: { ecmaVersion: 2024 }, - errors: [{ - messageId: "requireUFlag", - suggestions: [ - { - messageId: "addUFlag", - output: "/test/gimuy" - } - ] - }] - }, - { - code: "/test/", - options: [{ requiredUnicodeFlag: "u" }], - parserOptions: { ecmaVersion: 2024 }, - errors: [{ - messageId: "requireUFlag", - suggestions: [ - { - messageId: "addUFlag", - output: "/test/u" - } - ] - }] - }, - { - code: "/foo/gimy", - options: [{ requiredUnicodeFlag: "v" }], - errors: [{ - messageId: "requireVFlag", - suggestions: [ - { - messageId: "addVFlag", - output: "/foo/gimyv" - } - ] - }] - }, - { - code: "/foo/gimy", - options: [{ requiredUnicodeFlag: "u" }], - errors: [{ - messageId: "requireUFlag", - suggestions: [ - { - messageId: "addUFlag", - output: "/foo/gimyu" - } - ] - }] } ] }); From 144f73d138d3680d6a701cafaed1774baabb8489 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 29 Jul 2023 00:23:09 +0900 Subject: [PATCH 10/13] Update lib/rules/require-unicode-regexp.js Co-authored-by: Milos Djermanovic --- lib/rules/require-unicode-regexp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index 18fbec2ba46..dd687f8d796 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -28,7 +28,7 @@ module.exports = { type: "suggestion", docs: { - description: "Enforce the use of `u` and `v` flag on RegExp", + description: "Enforce the use of `u` or `v` flag on RegExp", recommended: false, url: "https://eslint.org/docs/latest/rules/require-unicode-regexp" }, From 713c0c73c12185216434abfb4f28d1c14d9a99d8 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 29 Jul 2023 00:23:21 +0900 Subject: [PATCH 11/13] Update docs/src/rules/require-unicode-regexp.md Co-authored-by: Milos Djermanovic --- docs/src/rules/require-unicode-regexp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/rules/require-unicode-regexp.md b/docs/src/rules/require-unicode-regexp.md index 325da84925d..969818e2c0d 100644 --- a/docs/src/rules/require-unicode-regexp.md +++ b/docs/src/rules/require-unicode-regexp.md @@ -51,7 +51,7 @@ The RegExp `v` flag, introduced in ECMAScript 2024, is a superset of the `u` fla Please see and for more details. -Therefore, the `u` and `v` flag lets us work better with regular expressions. +Therefore, the `u` and `v` flags let us work better with regular expressions. ## Rule Details From 082a792841a62229e9fef4eb5eab6aaf4ec42e11 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 29 Jul 2023 00:25:22 +0900 Subject: [PATCH 12/13] Update tests/lib/rules/require-unicode-regexp.js Co-authored-by: Milos Djermanovic --- tests/lib/rules/require-unicode-regexp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index 41187f2881b..4fcf84ed7e2 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -58,7 +58,7 @@ ruleTester.run("require-unicode-regexp", rule, { { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, - { code: "const flags = 'gimv'; new RegExp('foo', flags[4])", parserOptions: { ecmaVersion: 2024 } } + { code: "const flags = 'gimv'; new RegExp('foo', flags[3])", parserOptions: { ecmaVersion: 2024 } } ], invalid: [ { From 93b828ea29a1fa22e1b068f16f26a1de2208ade1 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 29 Jul 2023 00:36:13 +0900 Subject: [PATCH 13/13] Update tests/lib/rules/require-unicode-regexp.js Co-authored-by: Milos Djermanovic --- tests/lib/rules/require-unicode-regexp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index 4fcf84ed7e2..eab11b70fed 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -49,7 +49,7 @@ ruleTester.run("require-unicode-regexp", rule, { // for v flag { code: "/foo/v", parserOptions: { ecmaVersion: 2024 } }, - { code: "/foo/gimuy", parserOptions: { ecmaVersion: 2024 } }, + { code: "/foo/gimvy", parserOptions: { ecmaVersion: 2024 } }, { code: "RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, { code: "RegExp('', `v`)", parserOptions: { ecmaVersion: 2024 } }, { code: "new RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } },