/** * @fileoverview Rule to forbid control charactes from regular expressions. * @author Nicholas C. Zakas */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "disallow control characters in regular expressions", category: "Possible Errors", recommended: true }, schema: [] }, create(context) { /** * Get the regex expression * @param {ASTNode} node node to evaluate * @returns {*} Regex if found else null * @private */ function getRegExp(node) { if (node.value instanceof RegExp) { return node.value; } else 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; } } } 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 controlCharacters = getControlCharacters(computedValue); if (controlCharacters.length > 0) { context.report({ node, message: "Unexpected control character(s) in regular expression: {{controlChars}}.", data: { controlChars: controlCharacters.join(", ") } }); } } } }; } };