128 lines
4.1 KiB
JavaScript
128 lines
4.1 KiB
JavaScript
|
/**
|
||
|
* @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;
|
||
|
}
|
||
|
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(", ")
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
}
|
||
|
};
|