diff --git a/rules/better-regex.js b/rules/better-regex.js index 680b10b156..26ef86d761 100644 --- a/rules/better-regex.js +++ b/rules/better-regex.js @@ -28,10 +28,9 @@ const create = context => { } const {raw: original, regex} = node; - - // Regular Expressions with `u` flag are not well handled by `regexp-tree` + // Regular Expressions with `u` and `v` flag are not well handled by `regexp-tree` // https://github.com/DmitrySoshnikov/regexp-tree/issues/162 - if (regex.flags.includes('u')) { + if (regex.flags.includes('u') || regex.flags.includes('v')) { return; } diff --git a/rules/prefer-regexp-test.js b/rules/prefer-regexp-test.js index a1161ae324..ecff31099e 100644 --- a/rules/prefer-regexp-test.js +++ b/rules/prefer-regexp-test.js @@ -77,6 +77,10 @@ const cases = [ const isRegExpNode = node => isRegexLiteral(node) || isNewExpression(node, {name: 'RegExp'}); const isRegExpWithoutGlobalFlag = (node, scope) => { + if (isRegexLiteral(node)) { + return !node.regex.flags.includes('g'); + } + const staticResult = getStaticValue(node, scope); // Don't know if there is `g` flag diff --git a/rules/prefer-string-replace-all.js b/rules/prefer-string-replace-all.js index 3d2cbbe80c..cd0d01161b 100644 --- a/rules/prefer-string-replace-all.js +++ b/rules/prefer-string-replace-all.js @@ -17,7 +17,7 @@ function getPatternReplacement(node) { } const {pattern, flags} = node.regex; - if (flags.replace('u', '') !== 'g') { + if (flags.replace('u', '').replace('v', '') !== 'g') { return; } @@ -25,7 +25,8 @@ function getPatternReplacement(node) { try { tree = parseRegExp(pattern, flags, { - unicodePropertyEscape: true, + unicodePropertyEscape: flags.includes('u'), + unicodeSet: flags.includes('v'), namedGroups: true, lookbehind: true, }); diff --git a/test/better-regex.mjs b/test/better-regex.mjs index 1c6cefc18c..5675d04fba 100644 --- a/test/better-regex.mjs +++ b/test/better-regex.mjs @@ -40,6 +40,7 @@ test({ // Should not crash ESLint (#446 and #448) '/\\{\\{verificationUrl\\}\\}/gu', '/^test-(?[a-zA-Z-\\d]+)$/u', + String.raw`/[\p{Script_Extensions=Greek}--π]/v`, // Should not suggest wrong regex (#447) '/(\\s|\\.|@|_|-)/u', diff --git a/test/prefer-regexp-test.mjs b/test/prefer-regexp-test.mjs index 66c73d167e..459e38c781 100644 --- a/test/prefer-regexp-test.mjs +++ b/test/prefer-regexp-test.mjs @@ -150,6 +150,8 @@ test.snapshot({ let re = new RegExp('foo', 'g'); if(str.match(re)); `, + '!/a/u.exec(foo)', + '!/a/v.exec(foo)', ], }); @@ -173,3 +175,40 @@ test.vue({ }, ], }); + +const supportsUnicodeSets = (() => { + try { + // eslint-disable-next-line prefer-regex-literals -- Can't test with regex literal + return new RegExp('.', 'v').unicodeSets; + } catch {} + + return false; +})(); +// These cases can be auto-fixed in environments supports `v` flag (eg, Node.js v20), +// But will use suggestions instead in environments doesn't support `v` flag. +test({ + valid: [], + invalid: [ + { + code: 'const re = /a/v; !re.exec(foo)', + output: 'const re = /a/v; !re.test(foo)', + }, + { + code: 'const re = new RegExp("a", "v"); !re.exec(foo)', + output: 'const re = new RegExp("a", "v"); !re.test(foo)', + }, + ].map(({code, output}) => + supportsUnicodeSets + ? { + code, + output, + errors: 1, + } + : { + code, + errors: [ + {suggestions: [{output}]}, + ], + }, + ), +}); diff --git a/test/prefer-string-replace-all.mjs b/test/prefer-string-replace-all.mjs index e742f84c3b..6fb1158feb 100644 --- a/test/prefer-string-replace-all.mjs +++ b/test/prefer-string-replace-all.mjs @@ -77,11 +77,13 @@ test.snapshot({ 'foo.replace(/\\W/g, bar)', 'foo.replace(/\\u{61}/g, bar)', 'foo.replace(/\\u{61}/gu, bar)', + 'foo.replace(/\\u{61}/gv, bar)', 'foo.replace(/]/g, "bar")', // Extra flag 'foo.replace(/a/gi, bar)', 'foo.replace(/a/gui, bar)', 'foo.replace(/a/uig, bar)', + 'foo.replace(/a/vig, bar)', // Variables 'const pattern = new RegExp("foo", "g"); foo.replace(pattern, bar)', 'foo.replace(new RegExp("foo", "g"), bar)', @@ -99,9 +101,11 @@ test.snapshot({ 'foo.replace(/\\u{1f600}/gu, _)', 'foo.replace(/\\n/g, _)', 'foo.replace(/\\u{20}/gu, _)', + 'foo.replace(/\\u{20}/gv, _)', 'foo.replaceAll(/a]/g, _)', 'foo.replaceAll(/\\r\\n\\u{1f600}/gu, _)', + 'foo.replaceAll(/\\r\\n\\u{1f600}/gv, _)', `foo.replaceAll(/a${' very'.repeat(30)} long string/g, _)`, // Invalid RegExp #2010 diff --git a/test/prefer-string-starts-ends-with.mjs b/test/prefer-string-starts-ends-with.mjs index 9727322061..4af56909e5 100644 --- a/test/prefer-string-starts-ends-with.mjs +++ b/test/prefer-string-starts-ends-with.mjs @@ -219,6 +219,8 @@ test.snapshot({ '/a$/.test(a ??= b)', '/^a/.test(a || b)', '/^a/.test(a && b)', + '/^a/u.test("string")', + '/^a/v.test("string")', // eslint-disable-next-line no-template-curly-in-string '/a$/.test(`${unknown}`)', '/a$/.test(String(unknown))', diff --git a/test/snapshots/prefer-regexp-test.mjs.md b/test/snapshots/prefer-regexp-test.mjs.md index 4838a18635..92399bc611 100644 --- a/test/snapshots/prefer-regexp-test.mjs.md +++ b/test/snapshots/prefer-regexp-test.mjs.md @@ -927,3 +927,35 @@ Generated by [AVA](https://avajs.dev). 1 | let re = new RegExp('foo', 'g');␊ 2 | if(re.test(str));␊ ` + +## Invalid #57 + 1 | !/a/u.exec(foo) + +> Output + + `␊ + 1 | !/a/u.test(foo)␊ + ` + +> Error 1/1 + + `␊ + > 1 | !/a/u.exec(foo)␊ + | ^^^^ Prefer \`.test(…)\` over \`.exec(…)\`.␊ + ` + +## Invalid #58 + 1 | !/a/v.exec(foo) + +> Output + + `␊ + 1 | !/a/v.test(foo)␊ + ` + +> Error 1/1 + + `␊ + > 1 | !/a/v.exec(foo)␊ + | ^^^^ Prefer \`.test(…)\` over \`.exec(…)\`.␊ + ` diff --git a/test/snapshots/prefer-regexp-test.mjs.snap b/test/snapshots/prefer-regexp-test.mjs.snap index dc06b67f28..d692343591 100644 Binary files a/test/snapshots/prefer-regexp-test.mjs.snap and b/test/snapshots/prefer-regexp-test.mjs.snap differ diff --git a/test/snapshots/prefer-string-replace-all.mjs.md b/test/snapshots/prefer-string-replace-all.mjs.md index ead98c4bb3..e1b8d04b2a 100644 --- a/test/snapshots/prefer-string-replace-all.mjs.md +++ b/test/snapshots/prefer-string-replace-all.mjs.md @@ -263,6 +263,22 @@ Generated by [AVA](https://avajs.dev). ` ## Invalid #16 + 1 | foo.replace(/\u{61}/gv, bar) + +> Output + + `␊ + 1 | foo.replaceAll('a', bar)␊ + ` + +> Error 1/1 + + `␊ + > 1 | foo.replace(/\\u{61}/gv, bar)␊ + | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ + ` + +## Invalid #17 1 | foo.replace(/]/g, "bar") > Output @@ -278,7 +294,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #17 +## Invalid #18 1 | foo.replace(/a/gi, bar) > Output @@ -294,7 +310,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #18 +## Invalid #19 1 | foo.replace(/a/gui, bar) > Output @@ -310,7 +326,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #19 +## Invalid #20 1 | foo.replace(/a/uig, bar) > Output @@ -326,7 +342,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #20 +## Invalid #21 + 1 | foo.replace(/a/vig, bar) + +> Output + + `␊ + 1 | foo.replaceAll(/a/vig, bar)␊ + ` + +> Error 1/1 + + `␊ + > 1 | foo.replace(/a/vig, bar)␊ + | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ + ` + +## Invalid #22 1 | const pattern = new RegExp("foo", "g"); foo.replace(pattern, bar) > Output @@ -342,7 +374,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #21 +## Invalid #23 1 | foo.replace(new RegExp("foo", "g"), bar) > Output @@ -358,7 +390,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #22 +## Invalid #24 1 | foo.replace(/a]/g, _) > Output @@ -374,7 +406,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #23 +## Invalid #25 1 | foo.replace(/[a]/g, _) > Output @@ -390,7 +422,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #24 +## Invalid #26 1 | foo.replace(/a{1/g, _) > Output @@ -406,7 +438,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #25 +## Invalid #27 1 | foo.replace(/a{1}/g, _) > Output @@ -422,7 +454,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #26 +## Invalid #28 1 | foo.replace(/\u0022/g, _) > Output @@ -438,7 +470,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #27 +## Invalid #29 1 | foo.replace(/\u0027/g, _) > Output @@ -454,7 +486,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #28 +## Invalid #30 1 | foo.replace(/\cM\cj/g, _) > Output @@ -470,7 +502,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #29 +## Invalid #31 1 | foo.replace(/\x22/g, _) > Output @@ -486,7 +518,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #30 +## Invalid #32 1 | foo.replace(/\x27/g, _) > Output @@ -502,7 +534,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #31 +## Invalid #33 1 | foo.replace(/\uD83D\ude00/g, _) > Output @@ -518,7 +550,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #32 +## Invalid #34 1 | foo.replace(/\u{1f600}/gu, _) > Output @@ -534,7 +566,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #33 +## Invalid #35 1 | foo.replace(/\n/g, _) > Output @@ -550,7 +582,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #34 +## Invalid #36 1 | foo.replace(/\u{20}/gu, _) > Output @@ -566,7 +598,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ ` -## Invalid #35 +## Invalid #37 + 1 | foo.replace(/\u{20}/gv, _) + +> Output + + `␊ + 1 | foo.replaceAll(' ', _)␊ + ` + +> Error 1/1 + + `␊ + > 1 | foo.replace(/\\u{20}/gv, _)␊ + | ^^^^^^^ Prefer \`String#replaceAll()\` over \`String#replace()\`.␊ + ` + +## Invalid #38 1 | foo.replaceAll(/a]/g, _) > Output @@ -582,7 +630,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^ This pattern can be replaced with 'a]'.␊ ` -## Invalid #36 +## Invalid #39 1 | foo.replaceAll(/\r\n\u{1f600}/gu, _) > Output @@ -598,7 +646,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^ This pattern can be replaced with '\\r\\n😀'.␊ ` -## Invalid #37 +## Invalid #40 + 1 | foo.replaceAll(/\r\n\u{1f600}/gv, _) + +> Output + + `␊ + 1 | foo.replaceAll('\\r\\n😀', _)␊ + ` + +> Error 1/1 + + `␊ + > 1 | foo.replaceAll(/\\r\\n\\u{1f600}/gv, _)␊ + | ^^^^^^^^^^^^^^^^^ This pattern can be replaced with '\\r\\n😀'.␊ + ` + +## Invalid #41 1 | foo.replaceAll(/a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long string/g, _) > Output @@ -614,7 +678,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This pattern can be replaced with a string literal.␊ ` -## Invalid #38 +## Invalid #42 1 | foo.replace(/(?!a)+/g, "") > Output diff --git a/test/snapshots/prefer-string-replace-all.mjs.snap b/test/snapshots/prefer-string-replace-all.mjs.snap index 8e01a4e09c..baf0750efe 100644 Binary files a/test/snapshots/prefer-string-replace-all.mjs.snap and b/test/snapshots/prefer-string-replace-all.mjs.snap differ diff --git a/test/snapshots/prefer-string-starts-ends-with.mjs.md b/test/snapshots/prefer-string-starts-ends-with.mjs.md index 1b84db47ba..324dadbb72 100644 --- a/test/snapshots/prefer-string-starts-ends-with.mjs.md +++ b/test/snapshots/prefer-string-starts-ends-with.mjs.md @@ -573,6 +573,38 @@ Generated by [AVA](https://avajs.dev). ` ## Invalid #23 + 1 | /^a/u.test("string") + +> Output + + `␊ + 1 | "string".startsWith('a')␊ + ` + +> Error 1/1 + + `␊ + > 1 | /^a/u.test("string")␊ + | ^^^^^^^^^^^^^^^^^^^^ Prefer \`String#startsWith()\` over a regex with \`^\`.␊ + ` + +## Invalid #24 + 1 | /^a/v.test("string") + +> Output + + `␊ + 1 | "string".startsWith('a')␊ + ` + +> Error 1/1 + + `␊ + > 1 | /^a/v.test("string")␊ + | ^^^^^^^^^^^^^^^^^^^^ Prefer \`String#startsWith()\` over a regex with \`^\`.␊ + ` + +## Invalid #25 1 | /a$/.test(`${unknown}`) > Output @@ -588,7 +620,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`String#endsWith()\` over a regex with \`$\`.␊ ` -## Invalid #24 +## Invalid #26 1 | /a$/.test(String(unknown)) > Output @@ -604,7 +636,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`String#endsWith()\` over a regex with \`$\`.␊ ` -## Invalid #25 +## Invalid #27 1 | /* 1 */ 2 | ( 3 | /* 2 */ diff --git a/test/snapshots/prefer-string-starts-ends-with.mjs.snap b/test/snapshots/prefer-string-starts-ends-with.mjs.snap index 1f73e41958..9312b01345 100644 Binary files a/test/snapshots/prefer-string-starts-ends-with.mjs.snap and b/test/snapshots/prefer-string-starts-ends-with.mjs.snap differ