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 22 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
216 changes: 215 additions & 1 deletion lib/rules/prefer-regex-literals.js
Expand Up @@ -11,6 +11,8 @@

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
Expand Down Expand Up @@ -43,6 +45,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 +125,8 @@ module.exports = {
url: "https://eslint.org/docs/rules/prefer-regex-literals"
},

hasSuggestions: true,

schema: [
{
type: "object",
Expand All @@ -80,6 +149,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 All @@ -106,6 +177,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 +243,55 @@ module.exports = {
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 */
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
if (!ecmaVersion || ecmaVersion > 13 || ecmaVersion === 3 || ecmaVersion === 5) {
return 5;
}
return ecmaVersion + 2009;
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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,80 @@ 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 (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) {
let stringCodePointValue = String.fromCodePoint(characterNode.value);
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
const escaped = resolveEscapes(characterNode.raw);

if (escaped) {
stringCodePointValue = escaped;
regexContent =
regexContent.slice(0, characterNode.start + charIncrease) +
stringCodePointValue +
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
regexContent.slice(characterNode.end + charIncrease);

if (characterNode.raw.length === 1) {
charIncrease += 1;
}
}
}
});
}

const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;

context.report({
node,
messageId: "unexpectedRegExp",
suggest: noFix ? [] : [{
messageId: "unexpectedRegExp",
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
fix(fixer) {
const tokenAfter = sourceCode.getTokenAfter(node);

return fixer.replaceTextRange(
node.range,
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
(tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && /\S/u.test(text[node.range[0] - 1]) ? " " : "") +
newRegExpValue +
(tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && /\S/u.test(text[node.range[1]]) ? " " : "")
Yash-Singh1 marked this conversation as resolved.
Show resolved Hide resolved
);
}
}]
});
}
}
}
Expand Down