Permalink
Cannot retrieve contributors at this time
| /** | |
| * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval | |
| * @author James Allardice | |
| */ | |
| "use strict"; | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| docs: { | |
| description: "disallow the use of `eval()`-like methods", | |
| category: "Best Practices", | |
| recommended: false, | |
| url: "https://eslint.org/docs/rules/no-implied-eval" | |
| }, | |
| schema: [] | |
| }, | |
| create(context) { | |
| const CALLEE_RE = /^(setTimeout|setInterval|execScript)$/u; | |
| /* | |
| * Figures out if we should inspect a given binary expression. Is a stack | |
| * of stacks, where the first element in each substack is a CallExpression. | |
| */ | |
| const impliedEvalAncestorsStack = []; | |
| //-------------------------------------------------------------------------- | |
| // Helpers | |
| //-------------------------------------------------------------------------- | |
| /** | |
| * Get the last element of an array, without modifying arr, like pop(), but non-destructive. | |
| * @param {Array} arr What to inspect | |
| * @returns {*} The last element of arr | |
| * @private | |
| */ | |
| function last(arr) { | |
| return arr ? arr[arr.length - 1] : null; | |
| } | |
| /** | |
| * Checks if the given MemberExpression node is a potentially implied eval identifier on window. | |
| * @param {ASTNode} node The MemberExpression node to check. | |
| * @returns {boolean} Whether or not the given node is potentially an implied eval. | |
| * @private | |
| */ | |
| function isImpliedEvalMemberExpression(node) { | |
| const object = node.object, | |
| property = node.property, | |
| hasImpliedEvalName = CALLEE_RE.test(property.name) || CALLEE_RE.test(property.value); | |
| return object.name === "window" && hasImpliedEvalName; | |
| } | |
| /** | |
| * Determines if a node represents a call to a potentially implied eval. | |
| * | |
| * This checks the callee name and that there's an argument, but not the type of the argument. | |
| * @param {ASTNode} node The CallExpression to check. | |
| * @returns {boolean} True if the node matches, false if not. | |
| * @private | |
| */ | |
| function isImpliedEvalCallExpression(node) { | |
| const isMemberExpression = (node.callee.type === "MemberExpression"), | |
| isIdentifier = (node.callee.type === "Identifier"), | |
| isImpliedEvalCallee = | |
| (isIdentifier && CALLEE_RE.test(node.callee.name)) || | |
| (isMemberExpression && isImpliedEvalMemberExpression(node.callee)); | |
| return isImpliedEvalCallee && node.arguments.length; | |
| } | |
| /** | |
| * Checks that the parent is a direct descendent of an potential implied eval CallExpression, and if the parent is a CallExpression, that we're the first argument. | |
| * @param {ASTNode} node The node to inspect the parent of. | |
| * @returns {boolean} Was the parent a direct descendent, and is the child therefore potentially part of a dangerous argument? | |
| * @private | |
| */ | |
| function hasImpliedEvalParent(node) { | |
| // make sure our parent is marked | |
| return node.parent === last(last(impliedEvalAncestorsStack)) && | |
| // if our parent is a CallExpression, make sure we're the first argument | |
| (node.parent.type !== "CallExpression" || node === node.parent.arguments[0]); | |
| } | |
| /** | |
| * Checks if our parent is marked as part of an implied eval argument. If | |
| * so, collapses the top of impliedEvalAncestorsStack and reports on the | |
| * original CallExpression. | |
| * @param {ASTNode} node The CallExpression to check. | |
| * @returns {boolean} True if the node matches, false if not. | |
| * @private | |
| */ | |
| function checkString(node) { | |
| if (hasImpliedEvalParent(node)) { | |
| // remove the entire substack, to avoid duplicate reports | |
| const substack = impliedEvalAncestorsStack.pop(); | |
| context.report({ node: substack[0], message: "Implied eval. Consider passing a function instead of a string." }); | |
| } | |
| } | |
| //-------------------------------------------------------------------------- | |
| // Public | |
| //-------------------------------------------------------------------------- | |
| return { | |
| CallExpression(node) { | |
| if (isImpliedEvalCallExpression(node)) { | |
| // call expressions create a new substack | |
| impliedEvalAncestorsStack.push([node]); | |
| } | |
| }, | |
| "CallExpression:exit"(node) { | |
| if (node === last(last(impliedEvalAncestorsStack))) { | |
| /* | |
| * Destroys the entire sub-stack, rather than just using | |
| * last(impliedEvalAncestorsStack).pop(), as a CallExpression is | |
| * always the bottom of a impliedEvalAncestorsStack substack. | |
| */ | |
| impliedEvalAncestorsStack.pop(); | |
| } | |
| }, | |
| BinaryExpression(node) { | |
| if (node.operator === "+" && hasImpliedEvalParent(node)) { | |
| last(impliedEvalAncestorsStack).push(node); | |
| } | |
| }, | |
| "BinaryExpression:exit"(node) { | |
| if (node === last(last(impliedEvalAncestorsStack))) { | |
| last(impliedEvalAncestorsStack).pop(); | |
| } | |
| }, | |
| Literal(node) { | |
| if (typeof node.value === "string") { | |
| checkString(node); | |
| } | |
| }, | |
| TemplateLiteral(node) { | |
| checkString(node); | |
| } | |
| }; | |
| } | |
| }; |