Skip to content
Permalink
ed9506bbaf
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
Latest commit c96f843 Sep 14, 2020 History
0 contributors

Users who have contributed to this file

277 lines (232 sloc) 8.67 KB
/**
* @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",
"globalThis"
]);
/**
* 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 astUtils.isSpecificMemberAccess(node, null, 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,
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;
/*
* Optional call (`eval?.("code")`) is not direct eval.
* The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation
* But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation
*/
if (!node.optional && astUtils.isSpecificId(callee, "eval")) {
report(callee);
}
}
};
}
return {
"CallExpression:exit"(node) {
const callee = node.callee;
if (astUtils.isSpecificId(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);
}
}
};
}
};