diff --git a/lib/rules/no-misleading-character-class.js b/lib/rules/no-misleading-character-class.js index 70e31e604f4..ba936013249 100644 --- a/lib/rules/no-misleading-character-class.js +++ b/lib/rules/no-misleading-character-class.js @@ -108,6 +108,8 @@ module.exports = { url: "https://eslint.org/docs/rules/no-misleading-character-class" }, + hasSuggestions: true, + schema: [], messages: { @@ -115,7 +117,8 @@ module.exports = { combiningClass: "Unexpected combined character in character class.", emojiModifier: "Unexpected modified Emoji in character class.", regionalIndicatorSymbol: "Unexpected national flag in character class.", - zwj: "Unexpected joined character sequence in character class." + zwj: "Unexpected joined character sequence in character class.", + suggestUnicodeFlag: "Add unicode 'u' flag to regex." } }, create(context) { @@ -126,17 +129,10 @@ module.exports = { * @param {Node} node The node to report. * @param {string} pattern The regular expression pattern to verify. * @param {string} flags The flags of the regular expression. + * @param {Function} unicodeFixer Fixer for missing "u" flag. * @returns {void} */ - function verify(node, pattern, flags) { - const has = { - surrogatePairWithoutUFlag: false, - combiningClass: false, - variationSelector: false, - emojiModifier: false, - regionalIndicatorSymbol: false, - zwj: false - }; + function verify(node, pattern, flags, unicodeFixer) { let patternNode; try { @@ -152,26 +148,41 @@ module.exports = { return; } + const foundKinds = new Set(); + visitRegExpAST(patternNode, { onCharacterClassEnter(ccNode) { for (const chars of iterateCharacterSequence(ccNode.elements)) { for (const kind of kinds) { - has[kind] = has[kind] || hasCharacterSequence[kind](chars); + if (hasCharacterSequence[kind](chars)) { + foundKinds.add(kind); + } } } } }); - for (const kind of kinds) { - if (has[kind]) { - context.report({ node, messageId: kind }); + for (const kind of foundKinds) { + let suggest; + + if (kind === "surrogatePairWithoutUFlag") { + suggest = [{ + messageId: "suggestUnicodeFlag", + fix: unicodeFixer + }]; } + + context.report({ + node, + messageId: kind, + suggest + }); } } return { "Literal[regex]"(node) { - verify(node, node.regex.pattern, node.regex.flags); + verify(node, node.regex.pattern, node.regex.flags, fixer => fixer.insertTextAfter(node, "u")); }, "Program"() { const scope = context.getScope(); @@ -190,7 +201,19 @@ module.exports = { const flags = getStringIfConstant(flagsNode, scope); if (typeof pattern === "string") { - verify(node, pattern, flags || ""); + verify(node, pattern, flags || "", fixer => { + if (node.arguments.length === 1) { + return fixer.insertTextAfterRange(patternNode.range, ', "u"'); + } + + if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") { + const range = [flagsNode.range[0], flagsNode.range[1] - 1]; + + return fixer.insertTextAfterRange(range, "u"); + } + + return null; + }); } } } diff --git a/tests/lib/rules/no-misleading-character-class.js b/tests/lib/rules/no-misleading-character-class.js index a02e5e13e32..b7cf3738af5 100644 --- a/tests/lib/rules/no-misleading-character-class.js +++ b/tests/lib/rules/no-misleading-character-class.js @@ -76,11 +76,17 @@ ruleTester.run("no-misleading-character-class", rule, { // RegExp Literals. { code: "var r = /[πŸ‘]/", - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[πŸ‘]/u" }] + }] }, { code: "var r = /[\\uD83D\\uDC4D]/", - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[\\uD83D\\uDC4D]/u" }] + }] }, { code: "var r = /[Á]/", @@ -124,7 +130,10 @@ ruleTester.run("no-misleading-character-class", rule, { }, { code: "var r = /[πŸ‘ΆπŸ»]/", - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[πŸ‘ΆπŸ»]/u" }] + }] }, { code: "var r = /[πŸ‘ΆπŸ»]/u", @@ -140,7 +149,17 @@ ruleTester.run("no-misleading-character-class", rule, { }, { code: "var r = /[πŸ‡―πŸ‡΅]/", - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[πŸ‡―πŸ‡΅]/u" }] + }] + }, + { + code: "var r = /[πŸ‡―πŸ‡΅]/i", + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[πŸ‡―πŸ‡΅]/iu" }] + }] }, { code: "var r = /[πŸ‡―πŸ‡΅]/u", @@ -157,7 +176,10 @@ ruleTester.run("no-misleading-character-class", rule, { { code: "var r = /[πŸ‘¨β€πŸ‘©β€πŸ‘¦]/", errors: [ - { messageId: "surrogatePairWithoutUFlag" }, + { + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[πŸ‘¨β€πŸ‘©β€πŸ‘¦]/u" }] + }, { messageId: "zwj" } ] }, @@ -177,11 +199,17 @@ ruleTester.run("no-misleading-character-class", rule, { // RegExp constructors. { code: String.raw`var r = new RegExp("[πŸ‘]", "")`, - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[πŸ‘]", "u")` }] + }] }, { code: String.raw`var r = new RegExp("[\\uD83D\\uDC4D]", "")`, - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[\\uD83D\\uDC4D]", "u")` }] + }] }, { code: String.raw`var r = new RegExp("[Á]", "")`, @@ -225,7 +253,10 @@ ruleTester.run("no-misleading-character-class", rule, { }, { code: String.raw`var r = new RegExp("[πŸ‘ΆπŸ»]", "")`, - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[πŸ‘ΆπŸ»]", "u")` }] + }] }, { code: String.raw`var r = new RegExp("[πŸ‘ΆπŸ»]", "u")`, @@ -241,7 +272,24 @@ ruleTester.run("no-misleading-character-class", rule, { }, { code: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]", "")`, - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]", "u")` }] + }] + }, + { + code: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]", "i")`, + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]", "iu")` }] + }] + }, + { + code: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]")`, + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]", "u")` }] + }] }, { code: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]", "u")`, @@ -258,7 +306,10 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[πŸ‘¨β€πŸ‘©β€πŸ‘¦]", "")`, errors: [ - { messageId: "surrogatePairWithoutUFlag" }, + { + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("[πŸ‘¨β€πŸ‘©β€πŸ‘¦]", "u")` }] + }, { messageId: "zwj" } ] }, @@ -287,7 +338,10 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new globalThis.RegExp("[πŸ‡―πŸ‡΅]", "")`, env: { es2020: true }, - errors: [{ messageId: "surrogatePairWithoutUFlag" }] + errors: [{ + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new globalThis.RegExp("[πŸ‡―πŸ‡΅]", "u")` }] + }] }, { code: String.raw`var r = new globalThis.RegExp("[\\u{1F468}\\u{200D}\\u{1F469}\\u{200D}\\u{1F466}]", "u")`,