Skip to content

Commit

Permalink
handle RegExp calls with regex patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
fasttime committed Feb 25, 2024
1 parent 072f256 commit 6792594
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 9 deletions.
37 changes: 30 additions & 7 deletions lib/rules/no-misleading-character-class.js
Expand Up @@ -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");
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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)) {
Expand Down
81 changes: 79 additions & 2 deletions tests/lib/rules/no-misleading-character-class.js
Expand Up @@ -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]/",
Expand Down Expand Up @@ -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 } },
Expand Down Expand Up @@ -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');" }]
}]
},

Expand Down

0 comments on commit 6792594

Please sign in to comment.