diff --git a/lib/rules/no-control-regex.js b/lib/rules/no-control-regex.js
index e3afce787bd..24bb6be6671 100644
--- a/lib/rules/no-control-regex.js
+++ b/lib/rules/no-control-regex.js
@@ -5,6 +5,44 @@
"use strict";
+const RegExpValidator = require("regexpp").RegExpValidator;
+const collector = new class {
+ constructor() {
+ this.ecmaVersion = 2018;
+ this._source = "";
+ this._controlChars = [];
+ this._validator = new RegExpValidator(this);
+ }
+
+ onPatternEnter() {
+ this._controlChars = [];
+ }
+
+ onCharacter(start, end, cp) {
+ if (cp >= 0x00 &&
+ cp <= 0x1F &&
+ (
+ this._source.codePointAt(start) === cp ||
+ this._source.slice(start, end).startsWith("\\x") ||
+ this._source.slice(start, end).startsWith("\\u")
+ )
+ ) {
+ this._controlChars.push(`\\x${`0${cp.toString(16)}`.slice(-2)}`);
+ }
+ }
+
+ collectControlChars(regexpStr) {
+ try {
+ this._source = regexpStr;
+ this._validator.validatePattern(regexpStr); // Call onCharacter hook
+ } catch (err) {
+
+ // Ignore syntax errors in RegExp.
+ }
+ return this._controlChars;
+ }
+}();
+
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@@ -33,87 +71,28 @@ module.exports = {
* @returns {RegExp|null} Regex if found else null
* @private
*/
- function getRegExp(node) {
- if (node.value instanceof RegExp) {
- return node.value;
+ function getRegExpPattern(node) {
+ if (node.regex) {
+ return node.regex.pattern;
}
- if (typeof node.value === "string") {
-
- const parent = context.getAncestors().pop();
-
- if ((parent.type === "NewExpression" || parent.type === "CallExpression") &&
- parent.callee.type === "Identifier" && parent.callee.name === "RegExp"
- ) {
-
- // there could be an invalid regular expression string
- try {
- return new RegExp(node.value);
- } catch (ex) {
- return null;
- }
- }
+ if (typeof node.value === "string" &&
+ (node.parent.type === "NewExpression" || node.parent.type === "CallExpression") &&
+ node.parent.callee.type === "Identifier" &&
+ node.parent.callee.name === "RegExp" &&
+ node.parent.arguments[0] === node
+ ) {
+ return node.value;
}
return null;
}
-
- const controlChar = /[\x00-\x1f]/g; // eslint-disable-line no-control-regex
- const consecutiveSlashes = /\\+/g;
- const consecutiveSlashesAtEnd = /\\+$/g;
- const stringControlChar = /\\x[01][0-9a-f]/ig;
- const stringControlCharWithoutSlash = /x[01][0-9a-f]/ig;
-
- /**
- * Return a list of the control characters in the given regex string
- * @param {string} regexStr regex as string to check
- * @returns {array} returns a list of found control characters on given string
- * @private
- */
- function getControlCharacters(regexStr) {
-
- // check control characters, if RegExp object used
- const controlChars = regexStr.match(controlChar) || [];
-
- let stringControlChars = [];
-
- // check substr, if regex literal used
- const subStrIndex = regexStr.search(stringControlChar);
-
- if (subStrIndex > -1) {
-
- // is it escaped, check backslash count
- const possibleEscapeCharacters = regexStr.slice(0, subStrIndex).match(consecutiveSlashesAtEnd);
-
- const hasControlChars = possibleEscapeCharacters === null || !(possibleEscapeCharacters[0].length % 2);
-
- if (hasControlChars) {
- stringControlChars = regexStr.slice(subStrIndex, -1)
- .split(consecutiveSlashes)
- .filter(Boolean)
- .map(x => {
- const match = x.match(stringControlCharWithoutSlash) || [x];
-
- return `\\${match[0]}`;
- });
- }
- }
-
- return controlChars.map(x => {
- const hexCode = `0${x.charCodeAt(0).toString(16)}`.slice(-2);
-
- return `\\x${hexCode}`;
- }).concat(stringControlChars);
- }
-
return {
Literal(node) {
- const regex = getRegExp(node);
-
- if (regex) {
- const computedValue = regex.toString();
+ const pattern = getRegExpPattern(node);
- const controlCharacters = getControlCharacters(computedValue);
+ if (pattern) {
+ const controlCharacters = collector.collectControlChars(pattern);
if (controlCharacters.length > 0) {
context.report({
diff --git a/lib/rules/no-empty-character-class.js b/lib/rules/no-empty-character-class.js
index 1f5c7333aa0..e3f06b069a8 100644
--- a/lib/rules/no-empty-character-class.js
+++ b/lib/rules/no-empty-character-class.js
@@ -21,7 +21,7 @@
* 4. `[gimuy]*`: optional regexp flags
* 5. `$`: fix the match at the end of the string
*/
-const regex = /^\/([^\\[]|\\.|\[([^\\\]]|\\.)+])*\/[gimuy]*$/;
+const regex = /^\/([^\\[]|\\.|\[([^\\\]]|\\.)+])*\/[gimuys]*$/;
//------------------------------------------------------------------------------
// Rule Definition
diff --git a/lib/rules/no-invalid-regexp.js b/lib/rules/no-invalid-regexp.js
index 8ccb5242b00..e57029c9f82 100644
--- a/lib/rules/no-invalid-regexp.js
+++ b/lib/rules/no-invalid-regexp.js
@@ -8,7 +8,10 @@
// Requirements
//------------------------------------------------------------------------------
-const espree = require("espree");
+const RegExpValidator = require("regexpp").RegExpValidator;
+const validator = new RegExpValidator({ ecmaVersion: 2018 });
+const validFlags = /[gimuys]/g;
+const undefined1 = void 0;
//------------------------------------------------------------------------------
// Rule Definition
@@ -40,10 +43,14 @@ module.exports = {
create(context) {
const options = context.options[0];
- let allowedFlags = "";
+ let allowedFlags = null;
if (options && options.allowConstructorFlags) {
- allowedFlags = options.allowConstructorFlags.join("");
+ const temp = options.allowConstructorFlags.join("").replace(validFlags, "");
+
+ if (temp) {
+ allowedFlags = new RegExp(`[${temp}]`, "gi");
+ }
}
/**
@@ -57,51 +64,64 @@ module.exports = {
}
/**
- * Validate strings passed to the RegExp constructor
- * @param {ASTNode} node node to evaluate
- * @returns {void}
- * @private
+ * Check syntax error in a given pattern.
+ * @param {string} pattern The RegExp pattern to validate.
+ * @param {boolean} uFlag The Unicode flag.
+ * @returns {string|null} The syntax error.
+ */
+ function validateRegExpPattern(pattern, uFlag) {
+ try {
+ validator.validatePattern(pattern, undefined1, undefined1, uFlag);
+ return null;
+ } catch (err) {
+ return err.message;
+ }
+ }
+
+ /**
+ * Check syntax error in a given flags.
+ * @param {string} flags The RegExp flags to validate.
+ * @returns {string|null} The syntax error.
*/
- function check(node) {
- if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(node.arguments[0])) {
- let flags = isString(node.arguments[1]) ? node.arguments[1].value : "";
+ function validateRegExpFlags(flags) {
+ try {
+ validator.validateFlags(flags);
+ return null;
+ } catch (err) {
+ return `Invalid flags supplied to RegExp constructor '${flags}'`;
+ }
+ }
+
+ return {
+ "CallExpression, NewExpression"(node) {
+ if (node.callee.type !== "Identifier" || node.callee.name !== "RegExp" || !isString(node.arguments[0])) {
+ return;
+ }
+ const pattern = node.arguments[0].value;
+ let flags = "";
- if (allowedFlags) {
- flags = flags.replace(new RegExp(`[${allowedFlags}]`, "gi"), "");
+ if (node.arguments[1]) {
+ flags = isString(node.arguments[1]) ? node.arguments[1].value : null;
+ if (allowedFlags) {
+ flags = flags.replace(allowedFlags, "");
+ }
}
- try {
- void new RegExp(node.arguments[0].value);
- } catch (e) {
+ // If flags are unknown, check both are errored or not.
+ const message = validateRegExpFlags(flags) || (
+ (flags === null)
+ ? validateRegExpPattern(pattern, true) && validateRegExpPattern(pattern, false)
+ : validateRegExpPattern(pattern, flags.indexOf("u") !== -1)
+ );
+
+ if (message) {
context.report({
node,
message: "{{message}}.",
- data: e
+ data: { message }
});
}
-
- if (flags) {
-
- try {
- espree.parse(`/./${flags}`, context.parserOptions);
- } catch (ex) {
- context.report({
- node,
- message: "Invalid flags supplied to RegExp constructor '{{flags}}'.",
- data: {
- flags
- }
- });
- }
- }
-
}
- }
-
- return {
- CallExpression: check,
- NewExpression: check
};
-
}
};
diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js
index e36ec88b013..f1840aaf2df 100644
--- a/lib/rules/no-irregular-whitespace.js
+++ b/lib/rules/no-irregular-whitespace.js
@@ -101,7 +101,7 @@ module.exports = {
*/
function removeInvalidNodeErrorsInIdentifierOrLiteral(node) {
const shouldCheckStrings = skipStrings && (typeof node.value === "string");
- const shouldCheckRegExps = skipRegExps && (node.value instanceof RegExp);
+ const shouldCheckRegExps = skipRegExps && Boolean(node.regex);
if (shouldCheckStrings || shouldCheckRegExps) {
diff --git a/lib/rules/no-unexpected-multiline.js b/lib/rules/no-unexpected-multiline.js
index c7c26686d92..51ba7cfa75d 100644
--- a/lib/rules/no-unexpected-multiline.js
+++ b/lib/rules/no-unexpected-multiline.js
@@ -33,7 +33,7 @@ module.exports = {
const TAGGED_TEMPLATE_MESSAGE = "Unexpected newline between template tag and template literal.";
const DIVISION_MESSAGE = "Unexpected newline between numerator and division operator.";
- const REGEX_FLAG_MATCHER = /^[gimuy]+$/;
+ const REGEX_FLAG_MATCHER = /^[gimsuy]+$/;
const sourceCode = context.getSourceCode();
diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js
index 80abec78e84..efc9706f41d 100644
--- a/lib/rules/no-useless-escape.js
+++ b/lib/rules/no-useless-escape.js
@@ -25,8 +25,8 @@ function union(setA, setB) {
}
const VALID_STRING_ESCAPES = union(new Set("\\nrvtbfux"), astUtils.LINEBREAKS);
-const REGEX_GENERAL_ESCAPES = new Set("\\bcdDfnrsStvwWxu0123456789]");
-const REGEX_NON_CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("^/.$*+?[{}|()B"));
+const REGEX_GENERAL_ESCAPES = new Set("\\bcdDfnpPrsStvwWxu0123456789]");
+const REGEX_NON_CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("^/.$*+?[{}|()Bk"));
/**
* Parses a regular expression into a list of characters with character class info.
diff --git a/package.json b/package.json
index b5382e20047..4142400892b 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"doctrine": "^2.1.0",
"eslint-scope": "^3.7.1",
"eslint-visitor-keys": "^1.0.0",
- "espree": "^3.5.2",
+ "espree": "^3.5.4",
"esquery": "^1.0.0",
"esutils": "^2.0.2",
"file-entry-cache": "^2.0.0",
@@ -65,6 +65,7 @@
"path-is-inside": "^1.0.2",
"pluralize": "^7.0.0",
"progress": "^2.0.0",
+ "regexpp": "^1.0.1",
"require-uncached": "^1.0.3",
"semver": "^5.3.0",
"strip-ansi": "^4.0.0",
diff --git a/tests/lib/rules/no-control-regex.js b/tests/lib/rules/no-control-regex.js
index ebadafc3001..5b744e6e0bd 100644
--- a/tests/lib/rules/no-control-regex.js
+++ b/tests/lib/rules/no-control-regex.js
@@ -30,12 +30,17 @@ ruleTester.run("no-control-regex", rule, {
],
invalid: [
{ code: `var regex = ${/\x1f/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
- { code: `var regex = ${/\\\x1f\\x1e/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f, \\x1e" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
- { code: `var regex = ${/\\\x1fFOO\\x00/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f, \\x00" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
- { code: `var regex = ${/FOO\\\x1fFOO\\x1f/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f, \\x1f" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
+ { code: `var regex = ${/\\\x1f\\x1e/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
+ { code: `var regex = ${/\\\x1fFOO\\x00/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
+ { code: `var regex = ${/FOO\\\x1fFOO\\x1f/}`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] }, // eslint-disable-line no-control-regex
{ code: "var regex = new RegExp('\\x1f\\x1e')", errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f, \\x1e" }, type: "Literal" }] },
{ code: "var regex = new RegExp('\\x1fFOO\\x00')", errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f, \\x00" }, type: "Literal" }] },
{ code: "var regex = new RegExp('FOO\\x1fFOO\\x1f')", errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f, \\x1f" }, type: "Literal" }] },
- { code: "var regex = RegExp('\\x1f')", errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] }
+ { code: "var regex = RegExp('\\x1f')", errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] },
+ {
+ code: "var regex = /(?\\x1f)/",
+ parserOptions: { ecmaVersion: 2018 },
+ errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+ }
]
});
diff --git a/tests/lib/rules/no-empty-character-class.js b/tests/lib/rules/no-empty-character-class.js
index ce81e5488e6..2be8ae2a4e9 100644
--- a/tests/lib/rules/no-empty-character-class.js
+++ b/tests/lib/rules/no-empty-character-class.js
@@ -30,7 +30,9 @@ ruleTester.run("no-empty-character-class", rule, {
"var foo = /[\\[a-z[]]/;",
"var foo = /[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g;",
"var foo = /\\s*:\\s*/gim;",
- { code: "var foo = /[\\]]/uy;", parserOptions: { ecmaVersion: 6 } }
+ { code: "var foo = /[\\]]/uy;", parserOptions: { ecmaVersion: 6 } },
+ { code: "var foo = /[\\]]/s;", parserOptions: { ecmaVersion: 2018 } },
+ "var foo = /\\[]/"
],
invalid: [
{ code: "var foo = /^abc[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] },
diff --git a/tests/lib/rules/no-invalid-regexp.js b/tests/lib/rules/no-invalid-regexp.js
index f4f4b86e3b7..3b9dac536bd 100644
--- a/tests/lib/rules/no-invalid-regexp.js
+++ b/tests/lib/rules/no-invalid-regexp.js
@@ -32,13 +32,18 @@ ruleTester.run("no-invalid-regexp", rule, {
{ code: "new RegExp('.', 'u')", parserOptions: { ecmaVersion: 6 } },
{ code: "new RegExp('.', 'yu')", parserOptions: { ecmaVersion: 6 } },
{ code: "new RegExp('/', 'yu')", parserOptions: { ecmaVersion: 6 } },
- { code: "new RegExp('\\/', 'yu')", parserOptions: { ecmaVersion: 6 } }
+ { code: "new RegExp('\\/', 'yu')", parserOptions: { ecmaVersion: 6 } },
+ { code: "new RegExp('\\\\u{65}', 'u')", parserOptions: { ecmaVersion: 2015 } },
+ { code: "new RegExp('[\\\\u{0}-\\\\u{1F}]', 'u')", parserOptions: { ecmaVersion: 2015 } },
+ { code: "new RegExp('.', 's')", parserOptions: { ecmaVersion: 2018 } },
+ { code: "new RegExp('(?<=a)b')", parserOptions: { ecmaVersion: 2018 } },
+ { code: "new RegExp('(?b)\\k')", parserOptions: { ecmaVersion: 2018 } },
+ { code: "new RegExp('(?b)\\k', 'u')", parserOptions: { ecmaVersion: 2018 } },
+ { code: "new RegExp('\\\\p{Letter}', 'u')", parserOptions: { ecmaVersion: 2018 } }
],
invalid: [
{ code: "RegExp('[');", errors: [{ message: "Invalid regular expression: /[/: Unterminated character class.", type: "CallExpression" }] },
- { code: "RegExp('.', 'y');", errors: [{ message: "Invalid flags supplied to RegExp constructor 'y'.", type: "CallExpression" }] },
- { code: "RegExp('.', 'u');", errors: [{ message: "Invalid flags supplied to RegExp constructor 'u'.", type: "CallExpression" }] },
- { code: "RegExp('.', 'yu');", errors: [{ message: "Invalid flags supplied to RegExp constructor 'yu'.", type: "CallExpression" }] },
{ code: "RegExp('.', 'z');", errors: [{ message: "Invalid flags supplied to RegExp constructor 'z'.", type: "CallExpression" }] },
{ code: "new RegExp(')');", errors: [{ message: "Invalid regular expression: /)/: Unmatched ')'.", type: "NewExpression" }] }
]
diff --git a/tests/lib/rules/no-unexpected-multiline.js b/tests/lib/rules/no-unexpected-multiline.js
index 0cc8c2bdd0b..0df8de5573f 100644
--- a/tests/lib/rules/no-unexpected-multiline.js
+++ b/tests/lib/rules/no-unexpected-multiline.js
@@ -204,6 +204,17 @@ ruleTester.run("no-unexpected-multiline", rule, {
column: 17,
message: "Unexpected newline between numerator and division operator."
}]
+ },
+ {
+ code: `
+ foo
+ /bar/s.test(baz)
+ `,
+ errors: [{
+ line: 3,
+ column: 17,
+ message: "Unexpected newline between numerator and division operator."
+ }]
}
]
});
diff --git a/tests/lib/rules/no-useless-escape.js b/tests/lib/rules/no-useless-escape.js
index 054c1648846..b223099569b 100644
--- a/tests/lib/rules/no-useless-escape.js
+++ b/tests/lib/rules/no-useless-escape.js
@@ -113,7 +113,15 @@ ruleTester.run("no-useless-escape", rule, {
{ code: String.raw`/\]/u`, parserOptions: { ecmaVersion: 6 } },
String.raw`var foo = /foo\]/`,
String.raw`var foo = /[[]\]/`, // A character class containing '[', followed by a ']' character
- String.raw`var foo = /\[foo\.bar\]/`
+ String.raw`var foo = /\[foo\.bar\]/`,
+
+ // ES2018
+ { code: String.raw`var foo = /(?)\k/`, parserOptions: { ecmaVersion: 2018 } },
+ { code: String.raw`var foo = /(\\?)/`, parserOptions: { ecmaVersion: 2018 } },
+ { code: String.raw`var foo = /\p{ASCII}/u`, parserOptions: { ecmaVersion: 2018 } },
+ { code: String.raw`var foo = /\P{ASCII}/u`, parserOptions: { ecmaVersion: 2018 } },
+ { code: String.raw`var foo = /[\p{ASCII}]/u`, parserOptions: { ecmaVersion: 2018 } },
+ { code: String.raw`var foo = /[\P{ASCII}]/u`, parserOptions: { ecmaVersion: 2018 } }
],
invalid: [