]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | const candidatesOfGlobalObject = Object.freeze([ | |
19 | "global", | |
20 | "window", | |
21 | "globalThis" | |
22 | ]); | |
23 | ||
eb39fafa DC |
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 | */ | |
32 | function isMember(node, name) { | |
6f036462 | 33 | return astUtils.isSpecificMemberAccess(node, null, name); |
eb39fafa DC |
34 | } |
35 | ||
36 | //------------------------------------------------------------------------------ | |
37 | // Rule Definition | |
38 | //------------------------------------------------------------------------------ | |
39 | ||
40 | module.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 | ||
6f036462 TL |
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")) { | |
eb39fafa DC |
204 | report(callee); |
205 | } | |
206 | } | |
207 | }; | |
208 | } | |
209 | ||
210 | return { | |
211 | "CallExpression:exit"(node) { | |
212 | const callee = node.callee; | |
213 | ||
6f036462 | 214 | if (astUtils.isSpecificId(callee, "eval")) { |
eb39fafa DC |
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 | }; |