Skip to content

Commit

Permalink
New: Autofix support to prefer-regex-literals
Browse files Browse the repository at this point in the history
Fixes #15029
  • Loading branch information
Yash-Singh1 committed Sep 17, 2021
1 parent f87e199 commit d6d044a
Show file tree
Hide file tree
Showing 2 changed files with 1,034 additions and 44 deletions.
224 changes: 186 additions & 38 deletions lib/rules/prefer-regex-literals.js
Expand Up @@ -10,21 +10,13 @@
//------------------------------------------------------------------------------

const astUtils = require("./utils/ast-utils");
const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("eslint-utils");
const { CALL, CONSTRUCT, ReferenceTracker, findVariable, getStaticValue } = require("eslint-utils");
const { validateRegExpLiteral } = require("regexpp");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
* Determines whether the given node is a string literal.
* @param {ASTNode} node Node to check.
* @returns {boolean} True if the node is a string literal.
*/
function isStringLiteral(node) {
return node.type === "Literal" && typeof node.value === "string";
}

/**
* Determines whether the given node is a regex literal.
* @param {ASTNode} node Node to check.
Expand All @@ -43,6 +35,75 @@ function isStaticTemplateLiteral(node) {
return node.type === "TemplateLiteral" && node.expressions.length === 0;
}

const validPrecedingTokens = [
"(",
";",
"[",
",",
"=",
"+",
"*",
"-",
"?",
"~",
"%",
"**",
"!",
"typeof",
"instanceof",
"&&",
"||",
"??",
"await",
"yield",
"return",
"...",
"delete",
"void",
"in",
"<",
">",
"<=",
">=",
"==",
"===",
"!=",
"!==",
"<<",
">>",
">>>",
"&",
"|",
"^",
":",
"{",
"=>",
"*=",
"<<=",
">>=",
">>>=",
"^=",
"|=",
"&=",
"??=",
"||=",
"&&=",
"**=",
"+=",
"-=",
"/=",
"%=",
"/",
"do",
"break",
"continue",
"debugger",
"case",
"throw",
"of",
")"
];


//------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -58,6 +119,8 @@ module.exports = {
url: "https://eslint.org/docs/rules/prefer-regex-literals"
},

fixable: "code",

schema: [
{
type: "object",
Expand All @@ -80,6 +143,8 @@ module.exports = {

create(context) {
const [{ disallowRedundantWrapping = false } = {}] = context.options;
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();

/**
* Determines whether the given identifier node is a reference to a global variable.
Expand Down Expand Up @@ -107,53 +172,81 @@ module.exports = {
}

/**
* 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.
* Gets the value of a string
* @param {ASTNode} node The node to get the string of.
* @param {Scope} [scope] The scope
* @returns {string} The value of the node.
*/
function isStaticString(node) {
return isStringLiteral(node) ||
isStaticTemplateLiteral(node) ||
isStringRawTaggedStaticTemplateLiteral(node);
}
function getStringValue(node, scope) {
const result = getStaticValue(node, scope);

/**
* Determines whether the relevant arguments of the given are all static string literals.
* @param {ASTNode} node Node to check.
* @returns {boolean} True if all arguments are static strings.
*/
function hasOnlyStaticStringArguments(node) {
const args = node.arguments;

if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
return true;
if (result && typeof result.value === "string") {
return result.value;
}

return false;
return null;
}

/**
* Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
* @param {ASTNode} node Node to check.
* @param {Scope} scope The scope passed to getStringValue
* @returns {boolean} True if the node already contains a regex literal argument.
*/
function isUnnecessarilyWrappedRegexLiteral(node) {
function isUnnecessarilyWrappedRegexLiteral(node, scope) {
const args = node.arguments;

if (args.length === 1 && isRegexLiteral(args[0])) {
return true;
}

if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
if (args.length === 2 && isRegexLiteral(args[0]) && getStringValue(args[1], scope)) {
return true;
}

return false;
}

/* eslint-disable jsdoc/valid-types -- eslint-plugin-jsdoc's type parser doesn't support square brackets */
/**
* Returns a ecmaVersion compatible for regexpp.
* @param {import("../linter/linter").ParserOptions["ecmaVersion"]} ecmaVersion The ecmaVersion to convert.
* @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
*/
function getRegexppEcmaVersion(ecmaVersion) {
/* eslint-enable jsdoc/valid-types -- JSDoc is over, enable jsdoc/valid-types again */
if (ecmaVersion === 3 || ecmaVersion === 5) {
return 5;
}
return ecmaVersion + 2009;
}

/**
* Ensures that String is the only variable present in all child scopes
* @param {Scope} scope The scope to go within and remove variables from
* @param {boolean} [children] Whether to iterate over children or not and if false iterate through parents
* @returns {Scope} The newer scope with only String present
*/
function noStringScope(scope, children = true) {
scope.variables.filter(variable => variable.name !== "String").forEach(definedVariable => scope.set.delete(definedVariable.name));
if (children) {
for (const childScopeIndex in scope.childScopes) {
if (!isNaN(+childScopeIndex)) {
scope.childScopes[childScopeIndex] = noStringScope(scope.childScopes[childScopeIndex]);
}
}
if (scope.childScopes.length === 0 && scope.upper) {
scope.upper = noStringScope(scope.upper, false);
}
} else if (scope.upper) {
scope.upper = noStringScope(scope.upper, false);
}
return scope;
}

return {
Program() {
const scope = context.getScope();
let scope = context.getScope();

const tracker = new ReferenceTracker(scope);
const traceMap = {
RegExp: {
Expand All @@ -163,14 +256,69 @@ module.exports = {
};

for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
scope = noStringScope(scope);

if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node, scope)) {
if (node.arguments.length === 2) {
context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
context.report({
node,
messageId: "unexpectedRedundantRegExpWithFlags",
// eslint-disable-next-line no-loop-func -- scope value won't change
fix(fixer) {
return fixer.replaceTextRange(node.range, node.arguments[0].raw + getStringValue(node.arguments[1], scope));
}
});
} else {
context.report({ node, messageId: "unexpectedRedundantRegExp" });
context.report({
node,
messageId: "unexpectedRedundantRegExp",
fix(fixer) {
return fixer.replaceTextRange(node.range, node.arguments[0].raw);
}
});
}
} else if (hasOnlyStaticStringArguments(node)) {
context.report({ node, messageId: "unexpectedRegExp" });
} else if (
(getStringValue(node.arguments[0], scope) !== null) &&
(!node.arguments[1] || getStringValue(node.arguments[1], scope) !== null) &&
(node.arguments.length === 1 || node.arguments.length === 2)
) {
let regexContent = getStringValue(node.arguments[0], scope);

if (regexContent && !isStringRawTaggedStaticTemplateLiteral(node.arguments[0])) {
regexContent = regexContent.replace(/\\/gu, "\\").replace(/\r/gu, "\\r").replace(/\n/gu, "\\n").replace(/\t/gu, "\\t").replace(/\f/gu, "\\f").replace(/\v/gu, "\\v");
}

const newRegExpValue = `/${regexContent || "(?:)"}/${getStringValue(node.arguments[1], scope) || ""}`;

let noFix = false;

try {
validateRegExpLiteral(
newRegExpValue,
{ ecmaVersion: getRegexppEcmaVersion(context.parserOptions.ecmaVersion) }
);
} catch {
noFix = true;
}

const tokenBefore = sourceCode.getTokenBefore(node);

if (tokenBefore && !validPrecedingTokens.includes(tokenBefore.value)) {
noFix = true;
}

context.report({
node,
messageId: "unexpectedRegExp",
...(noFix ? {} : {
fix(fixer) {
return fixer.replaceTextRange(
node.range,
(text[node.range[0] - 1] === "/" ? " " : "") + newRegExpValue + (["in", "instanceof"].includes(sourceCode.getTokenAfter(node)?.value) && text[node.range[1]] === "i" ? " " : "")
);
}
})
});
}
}
}
Expand Down

0 comments on commit d6d044a

Please sign in to comment.