--- /dev/null
+/**
+ * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval
+ * @author James Allardice
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("./utils/ast-utils");
+const { getStaticValue } = require("eslint-utils");
+
+//------------------------------------------------------------------------------
+// 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: [],
+
+ messages: {
+ impliedEval: "Implied eval. Consider passing a function instead of a string."
+ }
+ },
+
+ create(context) {
+ const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]);
+ const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u;
+
+ /**
+ * Checks whether a node is evaluated as a string or not.
+ * @param {ASTNode} node A node to check.
+ * @returns {boolean} True if the node is evaluated as a string.
+ */
+ function isEvaluatedString(node) {
+ if (
+ (node.type === "Literal" && typeof node.value === "string") ||
+ node.type === "TemplateLiteral"
+ ) {
+ return true;
+ }
+ if (node.type === "BinaryExpression" && node.operator === "+") {
+ return isEvaluatedString(node.left) || isEvaluatedString(node.right);
+ }
+ return false;
+ }
+
+ /**
+ * Reports if the `CallExpression` node has evaluated argument.
+ * @param {ASTNode} node A CallExpression to check.
+ * @returns {void}
+ */
+ function reportImpliedEvalCallExpression(node) {
+ const [firstArgument] = node.arguments;
+
+ if (firstArgument) {
+
+ const staticValue = getStaticValue(firstArgument, context.getScope());
+ const isStaticString = staticValue && typeof staticValue.value === "string";
+ const isString = isStaticString || isEvaluatedString(firstArgument);
+
+ if (isString) {
+ context.report({
+ node,
+ messageId: "impliedEval"
+ });
+ }
+ }
+
+ }
+
+ /**
+ * Reports calls of `implied eval` via the global references.
+ * @param {Variable} globalVar A global variable to check.
+ * @returns {void}
+ */
+ function reportImpliedEvalViaGlobal(globalVar) {
+ const { references, name } = globalVar;
+
+ references.forEach(ref => {
+ const identifier = ref.identifier;
+ let node = identifier.parent;
+
+ while (astUtils.isSpecificMemberAccess(node, null, name)) {
+ node = node.parent;
+ }
+
+ if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) {
+ const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node;
+ const parent = calleeNode.parent;
+
+ if (parent.type === "CallExpression" && parent.callee === calleeNode) {
+ reportImpliedEvalCallExpression(parent);
+ }
+ }
+ });
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ return {
+ CallExpression(node) {
+ if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) {
+ reportImpliedEvalCallExpression(node);
+ }
+ },
+ "Program:exit"() {
+ const globalScope = context.getScope();
+
+ GLOBAL_CANDIDATES
+ .map(candidate => astUtils.getVariableByName(globalScope, candidate))
+ .filter(globalVar => !!globalVar && globalVar.defs.length === 0)
+ .forEach(reportImpliedEvalViaGlobal);
+ }
+ };
+
+ }
+};