Update ESLint and plugin packages
R=jacktfranklin@chromium.org
Bug: none
Change-Id: If1b2420ba6e1c100c3d6b2013815ff1a555ea987
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3369367
Auto-Submit: Tim Van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
diff --git a/node_modules/eslint/lib/rules/prefer-regex-literals.js b/node_modules/eslint/lib/rules/prefer-regex-literals.js
index fbfeb56..158f84b 100644
--- a/node_modules/eslint/lib/rules/prefer-regex-literals.js
+++ b/node_modules/eslint/lib/rules/prefer-regex-literals.js
@@ -11,11 +11,15 @@
const astUtils = require("./utils/ast-utils");
const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("eslint-utils");
+const { RegExpValidator, visitRegExpAST, RegExpParser } = require("regexpp");
+const { canTokensBeAdjacent } = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
+const REGEXPP_LATEST_ECMA_VERSION = 2022;
+
/**
* Determines whether the given node is a string literal.
* @param {ASTNode} node Node to check.
@@ -43,11 +47,77 @@
return node.type === "TemplateLiteral" && node.expressions.length === 0;
}
+const validPrecedingTokens = [
+ "(",
+ ";",
+ "[",
+ ",",
+ "=",
+ "+",
+ "*",
+ "-",
+ "?",
+ "~",
+ "%",
+ "**",
+ "!",
+ "typeof",
+ "instanceof",
+ "&&",
+ "||",
+ "??",
+ "return",
+ "...",
+ "delete",
+ "void",
+ "in",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "==",
+ "===",
+ "!=",
+ "!==",
+ "<<",
+ ">>",
+ ">>>",
+ "&",
+ "|",
+ "^",
+ ":",
+ "{",
+ "=>",
+ "*=",
+ "<<=",
+ ">>=",
+ ">>>=",
+ "^=",
+ "|=",
+ "&=",
+ "??=",
+ "||=",
+ "&&=",
+ "**=",
+ "+=",
+ "-=",
+ "/=",
+ "%=",
+ "/",
+ "do",
+ "break",
+ "continue",
+ "debugger",
+ "case",
+ "throw"
+];
+
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
+/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
@@ -58,6 +128,8 @@
url: "https://eslint.org/docs/rules/prefer-regex-literals"
},
+ hasSuggestions: true,
+
schema: [
{
type: "object",
@@ -73,6 +145,7 @@
messages: {
unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
+ replaceWithLiteral: "Replace with an equivalent regular expression literal.",
unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
}
@@ -80,6 +153,7 @@
create(context) {
const [{ disallowRedundantWrapping = false } = {}] = context.options;
+ const sourceCode = context.getSourceCode();
/**
* Determines whether the given identifier node is a reference to a global variable.
@@ -107,6 +181,27 @@
}
/**
+ * Gets the value of a string
+ * @param {ASTNode} node The node to get the string of.
+ * @returns {string|null} The value of the node.
+ */
+ function getStringValue(node) {
+ if (isStringLiteral(node)) {
+ return node.value;
+ }
+
+ if (isStaticTemplateLiteral(node)) {
+ return node.quasis[0].value.cooked;
+ }
+
+ if (isStringRawTaggedStaticTemplateLiteral(node)) {
+ return node.quasi.quasis[0].value.raw;
+ }
+
+ return null;
+ }
+
+ /**
* Determines whether the given node is considered to be a static string by the logic of this rule.
* @param {ASTNode} node Node to check.
* @returns {boolean} True if the node is a static string.
@@ -151,6 +246,53 @@
return false;
}
+ /**
+ * Returns a ecmaVersion compatible for regexpp.
+ * @param {any} ecmaVersion The ecmaVersion to convert.
+ * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
+ */
+ function getRegexppEcmaVersion(ecmaVersion) {
+ if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
+ return 5;
+ }
+ return Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION);
+ }
+
+ /**
+ * Makes a character escaped or else returns null.
+ * @param {string} character The character to escape.
+ * @returns {string} The resulting escaped character.
+ */
+ function resolveEscapes(character) {
+ switch (character) {
+ case "\n":
+ case "\\\n":
+ return "\\n";
+
+ case "\r":
+ case "\\\r":
+ return "\\r";
+
+ case "\t":
+ case "\\\t":
+ return "\\t";
+
+ case "\v":
+ case "\\\v":
+ return "\\v";
+
+ case "\f":
+ case "\\\f":
+ return "\\f";
+
+ case "/":
+ return "\\/";
+
+ default:
+ return null;
+ }
+ }
+
return {
Program() {
const scope = context.getScope();
@@ -170,7 +312,82 @@
context.report({ node, messageId: "unexpectedRedundantRegExp" });
}
} else if (hasOnlyStaticStringArguments(node)) {
- context.report({ node, messageId: "unexpectedRegExp" });
+ let regexContent = getStringValue(node.arguments[0]);
+ let noFix = false;
+ let flags;
+
+ if (node.arguments[1]) {
+ flags = getStringValue(node.arguments[1]);
+ }
+
+ const regexppEcmaVersion = getRegexppEcmaVersion(context.parserOptions.ecmaVersion);
+ const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
+
+ try {
+ RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
+ if (flags) {
+ RegExpValidatorInstance.validateFlags(flags);
+ }
+ } catch {
+ noFix = true;
+ }
+
+ const tokenBefore = sourceCode.getTokenBefore(node);
+
+ if (tokenBefore && !validPrecedingTokens.includes(tokenBefore.value)) {
+ noFix = true;
+ }
+
+ if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
+ noFix = true;
+ }
+
+ if (sourceCode.getCommentsInside(node).length > 0) {
+ noFix = true;
+ }
+
+ if (regexContent && !noFix) {
+ let charIncrease = 0;
+
+ const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
+
+ visitRegExpAST(ast, {
+ onCharacterEnter(characterNode) {
+ const escaped = resolveEscapes(characterNode.raw);
+
+ if (escaped) {
+ regexContent =
+ regexContent.slice(0, characterNode.start + charIncrease) +
+ escaped +
+ regexContent.slice(characterNode.end + charIncrease);
+
+ if (characterNode.raw.length === 1) {
+ charIncrease += 1;
+ }
+ }
+ }
+ });
+ }
+
+ const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
+
+ context.report({
+ node,
+ messageId: "unexpectedRegExp",
+ suggest: noFix ? [] : [{
+ messageId: "replaceWithLiteral",
+ fix(fixer) {
+ const tokenAfter = sourceCode.getTokenAfter(node);
+
+ return fixer.replaceText(
+ node,
+ (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
+ newRegExpValue +
+ (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "")
+ );
+ }
+ }]
+ });
}
}
}