]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval | |
3 | * @author James Allardice | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | const { getStaticValue } = require("eslint-utils"); | |
14 | ||
15 | //------------------------------------------------------------------------------ | |
16 | // Rule Definition | |
17 | //------------------------------------------------------------------------------ | |
18 | ||
19 | module.exports = { | |
20 | meta: { | |
21 | type: "suggestion", | |
22 | ||
23 | docs: { | |
24 | description: "disallow the use of `eval()`-like methods", | |
25 | category: "Best Practices", | |
26 | recommended: false, | |
27 | url: "https://eslint.org/docs/rules/no-implied-eval" | |
28 | }, | |
29 | ||
30 | schema: [], | |
31 | ||
32 | messages: { | |
33 | impliedEval: "Implied eval. Consider passing a function instead of a string." | |
34 | } | |
35 | }, | |
36 | ||
37 | create(context) { | |
38 | const EVAL_LIKE_FUNCS = Object.freeze(["setTimeout", "execScript", "setInterval"]); | |
39 | const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]); | |
40 | ||
41 | /** | |
42 | * Checks whether a node is evaluated as a string or not. | |
43 | * @param {ASTNode} node A node to check. | |
44 | * @returns {boolean} True if the node is evaluated as a string. | |
45 | */ | |
46 | function isEvaluatedString(node) { | |
47 | if ( | |
48 | (node.type === "Literal" && typeof node.value === "string") || | |
49 | node.type === "TemplateLiteral" | |
50 | ) { | |
51 | return true; | |
52 | } | |
53 | if (node.type === "BinaryExpression" && node.operator === "+") { | |
54 | return isEvaluatedString(node.left) || isEvaluatedString(node.right); | |
55 | } | |
56 | return false; | |
57 | } | |
58 | ||
59 | /** | |
60 | * Checks whether a node is an Identifier node named one of the specified names. | |
61 | * @param {ASTNode} node A node to check. | |
62 | * @param {string[]} specifiers Array of specified name. | |
63 | * @returns {boolean} True if the node is a Identifier node which has specified name. | |
64 | */ | |
65 | function isSpecifiedIdentifier(node, specifiers) { | |
66 | return node.type === "Identifier" && specifiers.includes(node.name); | |
67 | } | |
68 | ||
69 | /** | |
70 | * Checks a given node is a MemberExpression node which has the specified name's | |
71 | * property. | |
72 | * @param {ASTNode} node A node to check. | |
73 | * @param {string[]} specifiers Array of specified name. | |
74 | * @returns {boolean} `true` if the node is a MemberExpression node which has | |
75 | * the specified name's property | |
76 | */ | |
77 | function isSpecifiedMember(node, specifiers) { | |
78 | return node.type === "MemberExpression" && specifiers.includes(astUtils.getStaticPropertyName(node)); | |
79 | } | |
80 | ||
81 | /** | |
82 | * Reports if the `CallExpression` node has evaluated argument. | |
83 | * @param {ASTNode} node A CallExpression to check. | |
84 | * @returns {void} | |
85 | */ | |
86 | function reportImpliedEvalCallExpression(node) { | |
87 | const [firstArgument] = node.arguments; | |
88 | ||
89 | if (firstArgument) { | |
90 | ||
91 | const staticValue = getStaticValue(firstArgument, context.getScope()); | |
92 | const isStaticString = staticValue && typeof staticValue.value === "string"; | |
93 | const isString = isStaticString || isEvaluatedString(firstArgument); | |
94 | ||
95 | if (isString) { | |
96 | context.report({ | |
97 | node, | |
98 | messageId: "impliedEval" | |
99 | }); | |
100 | } | |
101 | } | |
102 | ||
103 | } | |
104 | ||
105 | /** | |
106 | * Reports calls of `implied eval` via the global references. | |
107 | * @param {Variable} globalVar A global variable to check. | |
108 | * @returns {void} | |
109 | */ | |
110 | function reportImpliedEvalViaGlobal(globalVar) { | |
111 | const { references, name } = globalVar; | |
112 | ||
113 | references.forEach(ref => { | |
114 | const identifier = ref.identifier; | |
115 | let node = identifier.parent; | |
116 | ||
117 | while (isSpecifiedMember(node, [name])) { | |
118 | node = node.parent; | |
119 | } | |
120 | ||
121 | if (isSpecifiedMember(node, EVAL_LIKE_FUNCS)) { | |
122 | const parent = node.parent; | |
123 | ||
124 | if (parent.type === "CallExpression" && parent.callee === node) { | |
125 | reportImpliedEvalCallExpression(parent); | |
126 | } | |
127 | } | |
128 | }); | |
129 | } | |
130 | ||
131 | //-------------------------------------------------------------------------- | |
132 | // Public | |
133 | //-------------------------------------------------------------------------- | |
134 | ||
135 | return { | |
136 | CallExpression(node) { | |
137 | if (isSpecifiedIdentifier(node.callee, EVAL_LIKE_FUNCS)) { | |
138 | reportImpliedEvalCallExpression(node); | |
139 | } | |
140 | }, | |
141 | "Program:exit"() { | |
142 | const globalScope = context.getScope(); | |
143 | ||
144 | GLOBAL_CANDIDATES | |
145 | .map(candidate => astUtils.getVariableByName(globalScope, candidate)) | |
146 | .filter(globalVar => !!globalVar && globalVar.defs.length === 0) | |
147 | .forEach(reportImpliedEvalViaGlobal); | |
148 | } | |
149 | }; | |
150 | ||
151 | } | |
152 | }; |