• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to flag use of eval() statement
3 * @author Nicholas C. Zakas
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const candidatesOfGlobalObject = Object.freeze([
19    "global",
20    "window",
21    "globalThis"
22]);
23
24/**
25 * Checks a given node is a MemberExpression node which has the specified name's
26 * property.
27 * @param {ASTNode} node A node to check.
28 * @param {string} name A name to check.
29 * @returns {boolean} `true` if the node is a MemberExpression node which has
30 *      the specified name's property
31 */
32function isMember(node, name) {
33    return astUtils.isSpecificMemberAccess(node, null, name);
34}
35
36//------------------------------------------------------------------------------
37// Rule Definition
38//------------------------------------------------------------------------------
39
40module.exports = {
41    meta: {
42        type: "suggestion",
43
44        docs: {
45            description: "disallow the use of `eval()`",
46            category: "Best Practices",
47            recommended: false,
48            url: "https://eslint.org/docs/rules/no-eval"
49        },
50
51        schema: [
52            {
53                type: "object",
54                properties: {
55                    allowIndirect: { type: "boolean", default: false }
56                },
57                additionalProperties: false
58            }
59        ],
60
61        messages: {
62            unexpected: "eval can be harmful."
63        }
64    },
65
66    create(context) {
67        const allowIndirect = Boolean(
68            context.options[0] &&
69            context.options[0].allowIndirect
70        );
71        const sourceCode = context.getSourceCode();
72        let funcInfo = null;
73
74        /**
75         * Pushs a variable scope (Program or Function) information to the stack.
76         *
77         * This is used in order to check whether or not `this` binding is a
78         * reference to the global object.
79         * @param {ASTNode} node A node of the scope. This is one of Program,
80         *      FunctionDeclaration, FunctionExpression, and ArrowFunctionExpression.
81         * @returns {void}
82         */
83        function enterVarScope(node) {
84            const strict = context.getScope().isStrict;
85
86            funcInfo = {
87                upper: funcInfo,
88                node,
89                strict,
90                defaultThis: false,
91                initialized: strict
92            };
93        }
94
95        /**
96         * Pops a variable scope from the stack.
97         * @returns {void}
98         */
99        function exitVarScope() {
100            funcInfo = funcInfo.upper;
101        }
102
103        /**
104         * Reports a given node.
105         *
106         * `node` is `Identifier` or `MemberExpression`.
107         * The parent of `node` might be `CallExpression`.
108         *
109         * The location of the report is always `eval` `Identifier` (or possibly
110         * `Literal`). The type of the report is `CallExpression` if the parent is
111         * `CallExpression`. Otherwise, it's the given node type.
112         * @param {ASTNode} node A node to report.
113         * @returns {void}
114         */
115        function report(node) {
116            const parent = node.parent;
117            const locationNode = node.type === "MemberExpression"
118                ? node.property
119                : node;
120
121            const reportNode = parent.type === "CallExpression" && parent.callee === node
122                ? parent
123                : node;
124
125            context.report({
126                node: reportNode,
127                loc: locationNode.loc,
128                messageId: "unexpected"
129            });
130        }
131
132        /**
133         * Reports accesses of `eval` via the global object.
134         * @param {eslint-scope.Scope} globalScope The global scope.
135         * @returns {void}
136         */
137        function reportAccessingEvalViaGlobalObject(globalScope) {
138            for (let i = 0; i < candidatesOfGlobalObject.length; ++i) {
139                const name = candidatesOfGlobalObject[i];
140                const variable = astUtils.getVariableByName(globalScope, name);
141
142                if (!variable) {
143                    continue;
144                }
145
146                const references = variable.references;
147
148                for (let j = 0; j < references.length; ++j) {
149                    const identifier = references[j].identifier;
150                    let node = identifier.parent;
151
152                    // To detect code like `window.window.eval`.
153                    while (isMember(node, name)) {
154                        node = node.parent;
155                    }
156
157                    // Reports.
158                    if (isMember(node, "eval")) {
159                        report(node);
160                    }
161                }
162            }
163        }
164
165        /**
166         * Reports all accesses of `eval` (excludes direct calls to eval).
167         * @param {eslint-scope.Scope} globalScope The global scope.
168         * @returns {void}
169         */
170        function reportAccessingEval(globalScope) {
171            const variable = astUtils.getVariableByName(globalScope, "eval");
172
173            if (!variable) {
174                return;
175            }
176
177            const references = variable.references;
178
179            for (let i = 0; i < references.length; ++i) {
180                const reference = references[i];
181                const id = reference.identifier;
182
183                if (id.name === "eval" && !astUtils.isCallee(id)) {
184
185                    // Is accessing to eval (excludes direct calls to eval)
186                    report(id);
187                }
188            }
189        }
190
191        if (allowIndirect) {
192
193            // Checks only direct calls to eval. It's simple!
194            return {
195                "CallExpression:exit"(node) {
196                    const callee = node.callee;
197
198                    /*
199                     * Optional call (`eval?.("code")`) is not direct eval.
200                     * The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation
201                     * But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation
202                     */
203                    if (!node.optional && astUtils.isSpecificId(callee, "eval")) {
204                        report(callee);
205                    }
206                }
207            };
208        }
209
210        return {
211            "CallExpression:exit"(node) {
212                const callee = node.callee;
213
214                if (astUtils.isSpecificId(callee, "eval")) {
215                    report(callee);
216                }
217            },
218
219            Program(node) {
220                const scope = context.getScope(),
221                    features = context.parserOptions.ecmaFeatures || {},
222                    strict =
223                        scope.isStrict ||
224                        node.sourceType === "module" ||
225                        (features.globalReturn && scope.childScopes[0].isStrict);
226
227                funcInfo = {
228                    upper: null,
229                    node,
230                    strict,
231                    defaultThis: true,
232                    initialized: true
233                };
234            },
235
236            "Program:exit"() {
237                const globalScope = context.getScope();
238
239                exitVarScope();
240                reportAccessingEval(globalScope);
241                reportAccessingEvalViaGlobalObject(globalScope);
242            },
243
244            FunctionDeclaration: enterVarScope,
245            "FunctionDeclaration:exit": exitVarScope,
246            FunctionExpression: enterVarScope,
247            "FunctionExpression:exit": exitVarScope,
248            ArrowFunctionExpression: enterVarScope,
249            "ArrowFunctionExpression:exit": exitVarScope,
250
251            ThisExpression(node) {
252                if (!isMember(node.parent, "eval")) {
253                    return;
254                }
255
256                /*
257                 * `this.eval` is found.
258                 * Checks whether or not the value of `this` is the global object.
259                 */
260                if (!funcInfo.initialized) {
261                    funcInfo.initialized = true;
262                    funcInfo.defaultThis = astUtils.isDefaultThisBinding(
263                        funcInfo.node,
264                        sourceCode
265                    );
266                }
267
268                if (!funcInfo.strict && funcInfo.defaultThis) {
269
270                    // `this.eval` is possible built-in `eval`.
271                    report(node.parent);
272                }
273            }
274        };
275
276    }
277};
278