diff --git a/lib/rules/no-misleading-character-class.js b/lib/rules/no-misleading-character-class.js index 8d548f4a334..2c525a6d960 100644 --- a/lib/rules/no-misleading-character-class.js +++ b/lib/rules/no-misleading-character-class.js @@ -3,7 +3,14 @@ */ "use strict"; -const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils"); +const { isRegExp } = require("node:util/types"); +const { + CALL, + CONSTRUCT, + ReferenceTracker, + getStaticValue, + getStringIfConstant +} = require("@eslint-community/eslint-utils"); const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp"); const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode"); const astUtils = require("./utils/ast-utils.js"); @@ -226,6 +233,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; const parser = new RegExpParser(); + const checkedPatternNodes = new Set(); /** * Verify a given regular expression. @@ -342,6 +350,9 @@ module.exports = { return { "Literal[regex]"(node) { + if (checkedPatternNodes.has(node)) { + return; + } verify(node, node.regex.pattern, node.regex.flags, fixer => { if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)) { return null; @@ -364,16 +375,28 @@ module.exports = { })) { let pattern, flags; const [patternNode, flagsNode] = refNode.arguments; + const evaluatedPattern = getStaticValue(patternNode, scope); - if (patternNode.type === "Literal" && patternNode.regex) { - pattern = patternNode.regex.pattern; - flags = flagsNode ? getStringIfConstant(flagsNode, scope) : patternNode.regex.flags; + if (!evaluatedPattern) { + continue; + } + if (flagsNode) { + if (isRegExp(evaluatedPattern.value)) { + pattern = evaluatedPattern.value.source; + checkedPatternNodes.add(patternNode); + } else { + pattern = String(evaluatedPattern.value); + } + flags = getStringIfConstant(flagsNode, scope); } else { - pattern = getStringIfConstant(patternNode, scope); - flags = flagsNode ? getStringIfConstant(flagsNode, scope) : ""; + if (isRegExp(evaluatedPattern.value)) { + continue; + } + pattern = String(evaluatedPattern.value); + flags = ""; } - if (typeof pattern === "string" && typeof flags === "string") { + if (typeof flags === "string") { verify(patternNode, pattern, flags, fixer => { if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)) { diff --git a/tests/lib/rules/no-misleading-character-class.js b/tests/lib/rules/no-misleading-character-class.js index 7e3d962e882..b6f71ff5374 100644 --- a/tests/lib/rules/no-misleading-character-class.js +++ b/tests/lib/rules/no-misleading-character-class.js @@ -41,6 +41,11 @@ ruleTester.run("no-misleading-character-class", rule, { "var r = /[JP]/", "var r = /πŸ‘¨β€πŸ‘©β€πŸ‘¦/", "var r = RegExp(/[πŸ‘]/u)", + "const regex = /[πŸ‘]/u; new RegExp(regex);", + { + code: "new RegExp('[πŸ‘]')", + languageOptions: { globals: { RegExp: "off" } } + }, // Ignore solo lead/tail surrogate. "var r = /[\\uD83D]/", @@ -80,6 +85,9 @@ ruleTester.run("no-misleading-character-class", rule, { "var r = new RegExp('[πŸ‡―πŸ‡΅]', `${foo}`)", String.raw`var r = new RegExp("[πŸ‘]", flags)`, + // don't report on spread arguments + "const args = ['[πŸ‘]', 'i']; new RegExp(...args);", + // ES2024 { code: "var r = /[πŸ‘]/v", languageOptions: { ecmaVersion: 2024 } }, { code: String.raw`var r = /^[\q{πŸ‘ΆπŸ»}]$/v`, languageOptions: { ecmaVersion: 2024 } }, @@ -1351,14 +1359,83 @@ ruleTester.run("no-misleading-character-class", rule, { }] }, - // second argument in RegExp should override flags in regexp literal + // second argument in RegExp should override flags in regex literal { code: "RegExp(/[aπŸ‘z]/u, '');", errors: [{ column: 11, endColumn: 13, messageId: "surrogatePairWithoutUFlag", - suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[aπŸ‘z]/u, 'u');" }] + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: "RegExp(/[aπŸ‘z]/u, 'u');" + }] + }] + }, + { + code: "const pattern = /[πŸ‘]/u; RegExp(pattern, '');", + errors: [{ + column: 33, + endColumn: 40, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: "const pattern = /[πŸ‘]/u; RegExp(pattern, 'u');" + }] + }] + }, + { + code: "const pattern = /[πŸ‘]/g; RegExp(pattern, 'i');", + errors: [{ + column: 19, + endColumn: 21, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: "const pattern = /[πŸ‘]/gu; RegExp(pattern, 'i');" + }] + }, { + column: 33, + endColumn: 40, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: "const pattern = /[πŸ‘]/g; RegExp(pattern, 'iu');" + }] + }] + }, + + // report only on regex literal if no flags are supplied + { + code: "RegExp(/[πŸ‘]/)", + errors: [{ + column: 10, + endColumn: 12, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[πŸ‘]/u)" }] + }] + }, + + // report only on RegExp call if a regex literal and flags are supplied + { + code: "RegExp(/[πŸ‘]/, 'i');", + errors: [{ + column: 10, + endColumn: 12, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[πŸ‘]/, 'iu');" }] + }] + }, + + // ignore RegExp if not built-in + { + code: "RegExp(/[πŸ‘]/, 'g');", + languageOptions: { globals: { RegExp: "off" } }, + errors: [{ + column: 10, + endColumn: 12, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[πŸ‘]/u, 'g');" }] }] },