You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
9.3 KiB
286 lines
9.3 KiB
/** |
|
* @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 |
|
//------------------------------------------------------------------------------ |
|
|
|
/** @type {import('../shared/types').Rule} */ |
|
module.exports = { |
|
meta: { |
|
type: "suggestion", |
|
|
|
docs: { |
|
description: "Disallow the use of `eval()`", |
|
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 `this` scope (non-arrow function, class static block, or class field initializer) information to the stack. |
|
* Top-level scopes are handled separately. |
|
* |
|
* 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. |
|
* For functions, this is one of FunctionDeclaration, FunctionExpression. |
|
* For class static blocks, this is StaticBlock. |
|
* For class field initializers, this can be any node that is PropertyDefinition#value. |
|
* @returns {void} |
|
*/ |
|
function enterThisScope(node) { |
|
const strict = context.getScope().isStrict; |
|
|
|
funcInfo = { |
|
upper: funcInfo, |
|
node, |
|
strict, |
|
isTopLevelOfScript: false, |
|
defaultThis: false, |
|
initialized: strict |
|
}; |
|
} |
|
|
|
/** |
|
* Pops a variable scope from the stack. |
|
* @returns {void} |
|
*/ |
|
function exitThisScope() { |
|
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), |
|
isTopLevelOfScript = node.sourceType !== "module" && !features.globalReturn; |
|
|
|
funcInfo = { |
|
upper: null, |
|
node, |
|
strict, |
|
isTopLevelOfScript, |
|
defaultThis: true, |
|
initialized: true |
|
}; |
|
}, |
|
|
|
"Program:exit"() { |
|
const globalScope = context.getScope(); |
|
|
|
exitThisScope(); |
|
reportAccessingEval(globalScope); |
|
reportAccessingEvalViaGlobalObject(globalScope); |
|
}, |
|
|
|
FunctionDeclaration: enterThisScope, |
|
"FunctionDeclaration:exit": exitThisScope, |
|
FunctionExpression: enterThisScope, |
|
"FunctionExpression:exit": exitThisScope, |
|
"PropertyDefinition > *.value": enterThisScope, |
|
"PropertyDefinition > *.value:exit": exitThisScope, |
|
StaticBlock: enterThisScope, |
|
"StaticBlock:exit": exitThisScope, |
|
|
|
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 |
|
); |
|
} |
|
|
|
// `this` at the top level of scripts always refers to the global object |
|
if (funcInfo.isTopLevelOfScript || (!funcInfo.strict && funcInfo.defaultThis)) { |
|
|
|
// `this.eval` is possible built-in `eval`. |
|
report(node.parent); |
|
} |
|
} |
|
}; |
|
|
|
} |
|
};
|
|
|