| /** |
| * @fileoverview Rule to flag use of eval() statement |
| * @author Nicholas C. Zakas |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const candidatesOfGlobalObject = Object.freeze([ |
| "global", |
| "window" |
| ]); |
| |
| /** |
| * Checks a given node is a Identifier node of the specified name. |
| * |
| * @param {ASTNode} node - A node to check. |
| * @param {string} name - A name to check. |
| * @returns {boolean} `true` if the node is a Identifier node of the name. |
| */ |
| function isIdentifier(node, name) { |
| return node.type === "Identifier" && node.name === name; |
| } |
| |
| /** |
| * Checks a given node is a Literal node of the specified string value. |
| * |
| * @param {ASTNode} node - A node to check. |
| * @param {string} name - A name to check. |
| * @returns {boolean} `true` if the node is a Literal node of the name. |
| */ |
| function isConstant(node, name) { |
| switch (node.type) { |
| case "Literal": |
| return node.value === name; |
| |
| case "TemplateLiteral": |
| return ( |
| node.expressions.length === 0 && |
| node.quasis[0].value.cooked === name |
| ); |
| |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Checks a given node is a MemberExpression node which has the specified name's |
| * property. |
| * |
| * @param {ASTNode} node - A node to check. |
| * @param {string} name - A name to check. |
| * @returns {boolean} `true` if the node is a MemberExpression node which has |
| * the specified name's property |
| */ |
| function isMember(node, name) { |
| return ( |
| node.type === "MemberExpression" && |
| (node.computed ? isConstant : isIdentifier)(node.property, name) |
| ); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "disallow the use of `eval()`", |
| category: "Best Practices", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/no-eval" |
| }, |
| |
| schema: [ |
| { |
| type: "object", |
| properties: { |
| allowIndirect: { type: "boolean", default: false } |
| }, |
| additionalProperties: false |
| } |
| ], |
| |
| messages: { |
| unexpected: "eval can be harmful." |
| } |
| }, |
| |
| create(context) { |
| const allowIndirect = Boolean( |
| context.options[0] && |
| context.options[0].allowIndirect |
| ); |
| const sourceCode = context.getSourceCode(); |
| let funcInfo = null; |
| |
| /** |
| * Pushs a variable scope (Program or Function) information to the stack. |
| * |
| * This is used in order to check whether or not `this` binding is a |
| * reference to the global object. |
| * |
| * @param {ASTNode} node - A node of the scope. This is one of Program, |
| * FunctionDeclaration, FunctionExpression, and ArrowFunctionExpression. |
| * @returns {void} |
| */ |
| function enterVarScope(node) { |
| const strict = context.getScope().isStrict; |
| |
| funcInfo = { |
| upper: funcInfo, |
| node, |
| strict, |
| defaultThis: false, |
| initialized: strict |
| }; |
| } |
| |
| /** |
| * Pops a variable scope from the stack. |
| * |
| * @returns {void} |
| */ |
| function exitVarScope() { |
| funcInfo = funcInfo.upper; |
| } |
| |
| /** |
| * Reports a given node. |
| * |
| * `node` is `Identifier` or `MemberExpression`. |
| * The parent of `node` might be `CallExpression`. |
| * |
| * The location of the report is always `eval` `Identifier` (or possibly |
| * `Literal`). The type of the report is `CallExpression` if the parent is |
| * `CallExpression`. Otherwise, it's the given node type. |
| * |
| * @param {ASTNode} node - A node to report. |
| * @returns {void} |
| */ |
| function report(node) { |
| const parent = node.parent; |
| const locationNode = node.type === "MemberExpression" |
| ? node.property |
| : node; |
| |
| const reportNode = parent.type === "CallExpression" && parent.callee === node |
| ? parent |
| : node; |
| |
| context.report({ |
| node: reportNode, |
| loc: locationNode.loc.start, |
| messageId: "unexpected" |
| }); |
| } |
| |
| /** |
| * Reports accesses of `eval` via the global object. |
| * |
| * @param {eslint-scope.Scope} globalScope - The global scope. |
| * @returns {void} |
| */ |
| function reportAccessingEvalViaGlobalObject(globalScope) { |
| for (let i = 0; i < candidatesOfGlobalObject.length; ++i) { |
| const name = candidatesOfGlobalObject[i]; |
| const variable = astUtils.getVariableByName(globalScope, name); |
| |
| if (!variable) { |
| continue; |
| } |
| |
| const references = variable.references; |
| |
| for (let j = 0; j < references.length; ++j) { |
| const identifier = references[j].identifier; |
| let node = identifier.parent; |
| |
| // To detect code like `window.window.eval`. |
| while (isMember(node, name)) { |
| node = node.parent; |
| } |
| |
| // Reports. |
| if (isMember(node, "eval")) { |
| report(node); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Reports all accesses of `eval` (excludes direct calls to eval). |
| * |
| * @param {eslint-scope.Scope} globalScope - The global scope. |
| * @returns {void} |
| */ |
| function reportAccessingEval(globalScope) { |
| const variable = astUtils.getVariableByName(globalScope, "eval"); |
| |
| if (!variable) { |
| return; |
| } |
| |
| const references = variable.references; |
| |
| for (let i = 0; i < references.length; ++i) { |
| const reference = references[i]; |
| const id = reference.identifier; |
| |
| if (id.name === "eval" && !astUtils.isCallee(id)) { |
| |
| // Is accessing to eval (excludes direct calls to eval) |
| report(id); |
| } |
| } |
| } |
| |
| if (allowIndirect) { |
| |
| // Checks only direct calls to eval. It's simple! |
| return { |
| "CallExpression:exit"(node) { |
| const callee = node.callee; |
| |
| if (isIdentifier(callee, "eval")) { |
| report(callee); |
| } |
| } |
| }; |
| } |
| |
| return { |
| "CallExpression:exit"(node) { |
| const callee = node.callee; |
| |
| if (isIdentifier(callee, "eval")) { |
| report(callee); |
| } |
| }, |
| |
| Program(node) { |
| const scope = context.getScope(), |
| features = context.parserOptions.ecmaFeatures || {}, |
| strict = |
| scope.isStrict || |
| node.sourceType === "module" || |
| (features.globalReturn && scope.childScopes[0].isStrict); |
| |
| funcInfo = { |
| upper: null, |
| node, |
| strict, |
| defaultThis: true, |
| initialized: true |
| }; |
| }, |
| |
| "Program:exit"() { |
| const globalScope = context.getScope(); |
| |
| exitVarScope(); |
| reportAccessingEval(globalScope); |
| reportAccessingEvalViaGlobalObject(globalScope); |
| }, |
| |
| FunctionDeclaration: enterVarScope, |
| "FunctionDeclaration:exit": exitVarScope, |
| FunctionExpression: enterVarScope, |
| "FunctionExpression:exit": exitVarScope, |
| ArrowFunctionExpression: enterVarScope, |
| "ArrowFunctionExpression:exit": exitVarScope, |
| |
| ThisExpression(node) { |
| if (!isMember(node.parent, "eval")) { |
| return; |
| } |
| |
| /* |
| * `this.eval` is found. |
| * Checks whether or not the value of `this` is the global object. |
| */ |
| if (!funcInfo.initialized) { |
| funcInfo.initialized = true; |
| funcInfo.defaultThis = astUtils.isDefaultThisBinding( |
| funcInfo.node, |
| sourceCode |
| ); |
| } |
| |
| if (!funcInfo.strict && funcInfo.defaultThis) { |
| |
| // `this.eval` is possible built-in `eval`. |
| report(node.parent); |
| } |
| } |
| }; |
| |
| } |
| }; |