Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Suggestions support for prefer-regex-literals #15077

Merged
merged 28 commits into from Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bac91cb
New: Autofix support to prefer-regex-literals
Yash-Singh1 Sep 16, 2021
dc34aa3
Merge branch 'master' into master
Yash-Singh1 Sep 17, 2021
52e6e00
Use canTokensBeAdjacent
Yash-Singh1 Sep 17, 2021
9ce5f47
Fix for NULL
Yash-Singh1 Sep 20, 2021
f2d582c
Switch to validatePattern and validateFlags
Yash-Singh1 Sep 20, 2021
ac05a36
Fix for unicode
Yash-Singh1 Sep 20, 2021
dbc3482
Apply a few suggestions from code review
Yash-Singh1 Sep 24, 2021
9e3b7e2
Fix: Double Escaping?
Yash-Singh1 Sep 25, 2021
4513700
Tests and fixes for no-unicode regexp
Yash-Singh1 Sep 25, 2021
64c556f
New: Drop usage of getStaticValue
Yash-Singh1 Sep 25, 2021
06109ba
Fix: Remove whitespace changes, fix jsdoc type, and convert to sugges…
Yash-Singh1 Sep 25, 2021
6cbcf4b
New: More test cases for .
Yash-Singh1 Sep 25, 2021
9ae6a20
Remove meta.docs.suggestion
Yash-Singh1 Oct 30, 2021
ea60c81
Fix linting
Yash-Singh1 Oct 30, 2021
5566402
Don't fix NULL
Yash-Singh1 Oct 30, 2021
852c5b6
Remove redundant wrapping suggestions for now
Yash-Singh1 Oct 31, 2021
ab9f17d
String.raw can have problematic chars
Yash-Singh1 Oct 31, 2021
4132ed9
Remove fixable
Yash-Singh1 Oct 31, 2021
9f42bdf
Fix messed up char increase
Yash-Singh1 Oct 31, 2021
8d96676
Apply suggestion from code review
Yash-Singh1 Dec 1, 2021
2ec405c
chore: use characterNode.raw instead of characterNode.value
Yash-Singh1 Dec 3, 2021
d4353a3
chore: do a bit of simplification of onCharacterEnter
Yash-Singh1 Dec 10, 2021
b4355d1
Apply suggestions from code review
Yash-Singh1 Dec 11, 2021
e292a77
chore: more changes following code review
Yash-Singh1 Dec 11, 2021
d6dcde5
chore: Use reliable way of testing if spacing needed
Yash-Singh1 Dec 11, 2021
300f349
diff msg for suggestion than main warning
Yash-Singh1 Dec 11, 2021
4fbf577
chore: stricter testing
Yash-Singh1 Dec 14, 2021
61bab7d
Apply suggestions from code review
Yash-Singh1 Dec 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
218 changes: 217 additions & 1 deletion lib/rules/prefer-regex-literals.js
Expand Up @@ -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.
Expand Down Expand Up @@ -43,6 +47,71 @@ function isStaticTemplateLiteral(node) {
return node.type === "TemplateLiteral" && node.expressions.length === 0;
}

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


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

hasSuggestions: true,

schema: [
{
type: "object",
Expand All @@ -73,13 +144,15 @@ module.exports = {

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."
}
},

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

/**
* Determines whether the given identifier node is a reference to a global variable.
Expand All @@ -106,6 +179,27 @@ module.exports = {
isStaticTemplateLiteral(node.quasi);
}

/**
* 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.
Expand Down Expand Up @@ -151,6 +245,53 @@ module.exports = {
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();
Expand All @@ -170,7 +311,82 @@ module.exports = {
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;
}

Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
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] ? " " : "")
);
}
}]
});
}
}
}
Expand Down