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