]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to control usage of strict mode directives. | |
3 | * @author Brandon Mills | |
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 | /** | |
19 | * Gets all of the Use Strict Directives in the Directive Prologue of a group of | |
20 | * statements. | |
21 | * @param {ASTNode[]} statements Statements in the program or function body. | |
22 | * @returns {ASTNode[]} All of the Use Strict Directives. | |
23 | */ | |
24 | function getUseStrictDirectives(statements) { | |
25 | const directives = []; | |
26 | ||
27 | for (let i = 0; i < statements.length; i++) { | |
28 | const statement = statements[i]; | |
29 | ||
30 | if ( | |
31 | statement.type === "ExpressionStatement" && | |
32 | statement.expression.type === "Literal" && | |
33 | statement.expression.value === "use strict" | |
34 | ) { | |
35 | directives[i] = statement; | |
36 | } else { | |
37 | break; | |
38 | } | |
39 | } | |
40 | ||
41 | return directives; | |
42 | } | |
43 | ||
44 | /** | |
45 | * Checks whether a given parameter is a simple parameter. | |
46 | * @param {ASTNode} node A pattern node to check. | |
47 | * @returns {boolean} `true` if the node is an Identifier node. | |
48 | */ | |
49 | function isSimpleParameter(node) { | |
50 | return node.type === "Identifier"; | |
51 | } | |
52 | ||
53 | /** | |
54 | * Checks whether a given parameter list is a simple parameter list. | |
55 | * @param {ASTNode[]} params A parameter list to check. | |
56 | * @returns {boolean} `true` if the every parameter is an Identifier node. | |
57 | */ | |
58 | function isSimpleParameterList(params) { | |
59 | return params.every(isSimpleParameter); | |
60 | } | |
61 | ||
62 | //------------------------------------------------------------------------------ | |
63 | // Rule Definition | |
64 | //------------------------------------------------------------------------------ | |
65 | ||
34eeec05 | 66 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
67 | module.exports = { |
68 | meta: { | |
69 | type: "suggestion", | |
70 | ||
71 | docs: { | |
8f9d1d4d | 72 | description: "Require or disallow strict mode directives", |
eb39fafa | 73 | recommended: false, |
f2a92ac6 | 74 | url: "https://eslint.org/docs/latest/rules/strict" |
eb39fafa DC |
75 | }, |
76 | ||
77 | schema: [ | |
78 | { | |
79 | enum: ["never", "global", "function", "safe"] | |
80 | } | |
81 | ], | |
82 | ||
83 | fixable: "code", | |
84 | messages: { | |
85 | function: "Use the function form of 'use strict'.", | |
86 | global: "Use the global form of 'use strict'.", | |
87 | multiple: "Multiple 'use strict' directives.", | |
88 | never: "Strict mode is not permitted.", | |
89 | unnecessary: "Unnecessary 'use strict' directive.", | |
90 | module: "'use strict' is unnecessary inside of modules.", | |
91 | implied: "'use strict' is unnecessary when implied strict mode is enabled.", | |
92 | unnecessaryInClasses: "'use strict' is unnecessary inside of classes.", | |
93 | nonSimpleParameterList: "'use strict' directive inside a function with non-simple parameter list throws a syntax error since ES2016.", | |
94 | wrap: "Wrap {{name}} in a function with 'use strict' directive." | |
95 | } | |
96 | }, | |
97 | ||
98 | create(context) { | |
99 | ||
100 | const ecmaFeatures = context.parserOptions.ecmaFeatures || {}, | |
101 | scopes = [], | |
102 | classScopes = []; | |
103 | let mode = context.options[0] || "safe"; | |
104 | ||
105 | if (ecmaFeatures.impliedStrict) { | |
106 | mode = "implied"; | |
107 | } else if (mode === "safe") { | |
f2a92ac6 | 108 | mode = ecmaFeatures.globalReturn || context.languageOptions.sourceType === "commonjs" ? "global" : "function"; |
eb39fafa DC |
109 | } |
110 | ||
111 | /** | |
112 | * Determines whether a reported error should be fixed, depending on the error type. | |
113 | * @param {string} errorType The type of error | |
114 | * @returns {boolean} `true` if the reported error should be fixed | |
115 | */ | |
116 | function shouldFix(errorType) { | |
117 | return errorType === "multiple" || errorType === "unnecessary" || errorType === "module" || errorType === "implied" || errorType === "unnecessaryInClasses"; | |
118 | } | |
119 | ||
120 | /** | |
121 | * Gets a fixer function to remove a given 'use strict' directive. | |
122 | * @param {ASTNode} node The directive that should be removed | |
123 | * @returns {Function} A fixer function | |
124 | */ | |
125 | function getFixFunction(node) { | |
126 | return fixer => fixer.remove(node); | |
127 | } | |
128 | ||
129 | /** | |
130 | * Report a slice of an array of nodes with a given message. | |
131 | * @param {ASTNode[]} nodes Nodes. | |
132 | * @param {string} start Index to start from. | |
133 | * @param {string} end Index to end before. | |
134 | * @param {string} messageId Message to display. | |
135 | * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) | |
136 | * @returns {void} | |
137 | */ | |
138 | function reportSlice(nodes, start, end, messageId, fix) { | |
139 | nodes.slice(start, end).forEach(node => { | |
140 | context.report({ node, messageId, fix: fix ? getFixFunction(node) : null }); | |
141 | }); | |
142 | } | |
143 | ||
144 | /** | |
145 | * Report all nodes in an array with a given message. | |
146 | * @param {ASTNode[]} nodes Nodes. | |
147 | * @param {string} messageId Message id to display. | |
148 | * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) | |
149 | * @returns {void} | |
150 | */ | |
151 | function reportAll(nodes, messageId, fix) { | |
152 | reportSlice(nodes, 0, nodes.length, messageId, fix); | |
153 | } | |
154 | ||
155 | /** | |
156 | * Report all nodes in an array, except the first, with a given message. | |
157 | * @param {ASTNode[]} nodes Nodes. | |
158 | * @param {string} messageId Message id to display. | |
159 | * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) | |
160 | * @returns {void} | |
161 | */ | |
162 | function reportAllExceptFirst(nodes, messageId, fix) { | |
163 | reportSlice(nodes, 1, nodes.length, messageId, fix); | |
164 | } | |
165 | ||
166 | /** | |
167 | * Entering a function in 'function' mode pushes a new nested scope onto the | |
168 | * stack. The new scope is true if the nested function is strict mode code. | |
169 | * @param {ASTNode} node The function declaration or expression. | |
170 | * @param {ASTNode[]} useStrictDirectives The Use Strict Directives of the node. | |
171 | * @returns {void} | |
172 | */ | |
173 | function enterFunctionInFunctionMode(node, useStrictDirectives) { | |
174 | const isInClass = classScopes.length > 0, | |
175 | isParentGlobal = scopes.length === 0 && classScopes.length === 0, | |
176 | isParentStrict = scopes.length > 0 && scopes[scopes.length - 1], | |
177 | isStrict = useStrictDirectives.length > 0; | |
178 | ||
179 | if (isStrict) { | |
180 | if (!isSimpleParameterList(node.params)) { | |
181 | context.report({ node: useStrictDirectives[0], messageId: "nonSimpleParameterList" }); | |
182 | } else if (isParentStrict) { | |
183 | context.report({ node: useStrictDirectives[0], messageId: "unnecessary", fix: getFixFunction(useStrictDirectives[0]) }); | |
184 | } else if (isInClass) { | |
185 | context.report({ node: useStrictDirectives[0], messageId: "unnecessaryInClasses", fix: getFixFunction(useStrictDirectives[0]) }); | |
186 | } | |
187 | ||
188 | reportAllExceptFirst(useStrictDirectives, "multiple", true); | |
189 | } else if (isParentGlobal) { | |
190 | if (isSimpleParameterList(node.params)) { | |
191 | context.report({ node, messageId: "function" }); | |
192 | } else { | |
193 | context.report({ | |
194 | node, | |
195 | messageId: "wrap", | |
196 | data: { name: astUtils.getFunctionNameWithKind(node) } | |
197 | }); | |
198 | } | |
199 | } | |
200 | ||
201 | scopes.push(isParentStrict || isStrict); | |
202 | } | |
203 | ||
204 | /** | |
205 | * Exiting a function in 'function' mode pops its scope off the stack. | |
206 | * @returns {void} | |
207 | */ | |
208 | function exitFunctionInFunctionMode() { | |
209 | scopes.pop(); | |
210 | } | |
211 | ||
212 | /** | |
213 | * Enter a function and either: | |
214 | * - Push a new nested scope onto the stack (in 'function' mode). | |
215 | * - Report all the Use Strict Directives (in the other modes). | |
216 | * @param {ASTNode} node The function declaration or expression. | |
217 | * @returns {void} | |
218 | */ | |
219 | function enterFunction(node) { | |
220 | const isBlock = node.body.type === "BlockStatement", | |
221 | useStrictDirectives = isBlock | |
222 | ? getUseStrictDirectives(node.body.body) : []; | |
223 | ||
224 | if (mode === "function") { | |
225 | enterFunctionInFunctionMode(node, useStrictDirectives); | |
226 | } else if (useStrictDirectives.length > 0) { | |
227 | if (isSimpleParameterList(node.params)) { | |
228 | reportAll(useStrictDirectives, mode, shouldFix(mode)); | |
229 | } else { | |
230 | context.report({ node: useStrictDirectives[0], messageId: "nonSimpleParameterList" }); | |
231 | reportAllExceptFirst(useStrictDirectives, "multiple", true); | |
232 | } | |
233 | } | |
234 | } | |
235 | ||
236 | const rule = { | |
237 | Program(node) { | |
238 | const useStrictDirectives = getUseStrictDirectives(node.body); | |
239 | ||
240 | if (node.sourceType === "module") { | |
241 | mode = "module"; | |
242 | } | |
243 | ||
244 | if (mode === "global") { | |
245 | if (node.body.length > 0 && useStrictDirectives.length === 0) { | |
246 | context.report({ node, messageId: "global" }); | |
247 | } | |
248 | reportAllExceptFirst(useStrictDirectives, "multiple", true); | |
249 | } else { | |
250 | reportAll(useStrictDirectives, mode, shouldFix(mode)); | |
251 | } | |
252 | }, | |
253 | FunctionDeclaration: enterFunction, | |
254 | FunctionExpression: enterFunction, | |
255 | ArrowFunctionExpression: enterFunction | |
256 | }; | |
257 | ||
258 | if (mode === "function") { | |
259 | Object.assign(rule, { | |
260 | ||
261 | // Inside of class bodies are always strict mode. | |
262 | ClassBody() { | |
263 | classScopes.push(true); | |
264 | }, | |
265 | "ClassBody:exit"() { | |
266 | classScopes.pop(); | |
267 | }, | |
268 | ||
269 | "FunctionDeclaration:exit": exitFunctionInFunctionMode, | |
270 | "FunctionExpression:exit": exitFunctionInFunctionMode, | |
271 | "ArrowFunctionExpression:exit": exitFunctionInFunctionMode | |
272 | }); | |
273 | } | |
274 | ||
275 | return rule; | |
276 | } | |
277 | }; |