1 | /**
|
---|
2 | * @fileoverview A rule to disallow using `this`/`super` before `super()`.
|
---|
3 | * @author Toru Nagashima
|
---|
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 | * Checks whether or not a given node is a constructor.
|
---|
20 | * @param {ASTNode} node A node to check. This node type is one of
|
---|
21 | * `Program`, `FunctionDeclaration`, `FunctionExpression`, and
|
---|
22 | * `ArrowFunctionExpression`.
|
---|
23 | * @returns {boolean} `true` if the node is a constructor.
|
---|
24 | */
|
---|
25 | function isConstructorFunction(node) {
|
---|
26 | return (
|
---|
27 | node.type === "FunctionExpression" &&
|
---|
28 | node.parent.type === "MethodDefinition" &&
|
---|
29 | node.parent.kind === "constructor"
|
---|
30 | );
|
---|
31 | }
|
---|
32 |
|
---|
33 | //------------------------------------------------------------------------------
|
---|
34 | // Rule Definition
|
---|
35 | //------------------------------------------------------------------------------
|
---|
36 |
|
---|
37 | /** @type {import('../shared/types').Rule} */
|
---|
38 | module.exports = {
|
---|
39 | meta: {
|
---|
40 | type: "problem",
|
---|
41 |
|
---|
42 | docs: {
|
---|
43 | description: "Disallow `this`/`super` before calling `super()` in constructors",
|
---|
44 | recommended: true,
|
---|
45 | url: "https://eslint.org/docs/latest/rules/no-this-before-super"
|
---|
46 | },
|
---|
47 |
|
---|
48 | schema: [],
|
---|
49 |
|
---|
50 | messages: {
|
---|
51 | noBeforeSuper: "'{{kind}}' is not allowed before 'super()'."
|
---|
52 | }
|
---|
53 | },
|
---|
54 |
|
---|
55 | create(context) {
|
---|
56 |
|
---|
57 | /*
|
---|
58 | * Information for each constructor.
|
---|
59 | * - upper: Information of the upper constructor.
|
---|
60 | * - hasExtends: A flag which shows whether the owner class has a valid
|
---|
61 | * `extends` part.
|
---|
62 | * - scope: The scope of the owner class.
|
---|
63 | * - codePath: The code path of this constructor.
|
---|
64 | */
|
---|
65 | let funcInfo = null;
|
---|
66 |
|
---|
67 | /*
|
---|
68 | * Information for each code path segment.
|
---|
69 | * Each key is the id of a code path segment.
|
---|
70 | * Each value is an object:
|
---|
71 | * - superCalled: The flag which shows `super()` called in all code paths.
|
---|
72 | * - invalidNodes: The array of invalid ThisExpression and Super nodes.
|
---|
73 | */
|
---|
74 | let segInfoMap = Object.create(null);
|
---|
75 |
|
---|
76 | /**
|
---|
77 | * Gets whether or not `super()` is called in a given code path segment.
|
---|
78 | * @param {CodePathSegment} segment A code path segment to get.
|
---|
79 | * @returns {boolean} `true` if `super()` is called.
|
---|
80 | */
|
---|
81 | function isCalled(segment) {
|
---|
82 | return !segment.reachable || segInfoMap[segment.id].superCalled;
|
---|
83 | }
|
---|
84 |
|
---|
85 | /**
|
---|
86 | * Checks whether or not this is in a constructor.
|
---|
87 | * @returns {boolean} `true` if this is in a constructor.
|
---|
88 | */
|
---|
89 | function isInConstructorOfDerivedClass() {
|
---|
90 | return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends);
|
---|
91 | }
|
---|
92 |
|
---|
93 | /**
|
---|
94 | * Determines if every segment in a set has been called.
|
---|
95 | * @param {Set<CodePathSegment>} segments The segments to search.
|
---|
96 | * @returns {boolean} True if every segment has been called; false otherwise.
|
---|
97 | */
|
---|
98 | function isEverySegmentCalled(segments) {
|
---|
99 | for (const segment of segments) {
|
---|
100 | if (!isCalled(segment)) {
|
---|
101 | return false;
|
---|
102 | }
|
---|
103 | }
|
---|
104 |
|
---|
105 | return true;
|
---|
106 | }
|
---|
107 |
|
---|
108 | /**
|
---|
109 | * Checks whether or not this is before `super()` is called.
|
---|
110 | * @returns {boolean} `true` if this is before `super()` is called.
|
---|
111 | */
|
---|
112 | function isBeforeCallOfSuper() {
|
---|
113 | return (
|
---|
114 | isInConstructorOfDerivedClass() &&
|
---|
115 | !isEverySegmentCalled(funcInfo.currentSegments)
|
---|
116 | );
|
---|
117 | }
|
---|
118 |
|
---|
119 | /**
|
---|
120 | * Sets a given node as invalid.
|
---|
121 | * @param {ASTNode} node A node to set as invalid. This is one of
|
---|
122 | * a ThisExpression and a Super.
|
---|
123 | * @returns {void}
|
---|
124 | */
|
---|
125 | function setInvalid(node) {
|
---|
126 | const segments = funcInfo.currentSegments;
|
---|
127 |
|
---|
128 | for (const segment of segments) {
|
---|
129 | if (segment.reachable) {
|
---|
130 | segInfoMap[segment.id].invalidNodes.push(node);
|
---|
131 | }
|
---|
132 | }
|
---|
133 | }
|
---|
134 |
|
---|
135 | /**
|
---|
136 | * Sets the current segment as `super` was called.
|
---|
137 | * @returns {void}
|
---|
138 | */
|
---|
139 | function setSuperCalled() {
|
---|
140 | const segments = funcInfo.currentSegments;
|
---|
141 |
|
---|
142 | for (const segment of segments) {
|
---|
143 | if (segment.reachable) {
|
---|
144 | segInfoMap[segment.id].superCalled = true;
|
---|
145 | }
|
---|
146 | }
|
---|
147 | }
|
---|
148 |
|
---|
149 | return {
|
---|
150 |
|
---|
151 | /**
|
---|
152 | * Adds information of a constructor into the stack.
|
---|
153 | * @param {CodePath} codePath A code path which was started.
|
---|
154 | * @param {ASTNode} node The current node.
|
---|
155 | * @returns {void}
|
---|
156 | */
|
---|
157 | onCodePathStart(codePath, node) {
|
---|
158 | if (isConstructorFunction(node)) {
|
---|
159 |
|
---|
160 | // Class > ClassBody > MethodDefinition > FunctionExpression
|
---|
161 | const classNode = node.parent.parent.parent;
|
---|
162 |
|
---|
163 | funcInfo = {
|
---|
164 | upper: funcInfo,
|
---|
165 | isConstructor: true,
|
---|
166 | hasExtends: Boolean(
|
---|
167 | classNode.superClass &&
|
---|
168 | !astUtils.isNullOrUndefined(classNode.superClass)
|
---|
169 | ),
|
---|
170 | codePath,
|
---|
171 | currentSegments: new Set()
|
---|
172 | };
|
---|
173 | } else {
|
---|
174 | funcInfo = {
|
---|
175 | upper: funcInfo,
|
---|
176 | isConstructor: false,
|
---|
177 | hasExtends: false,
|
---|
178 | codePath,
|
---|
179 | currentSegments: new Set()
|
---|
180 | };
|
---|
181 | }
|
---|
182 | },
|
---|
183 |
|
---|
184 | /**
|
---|
185 | * Removes the top of stack item.
|
---|
186 | *
|
---|
187 | * And this traverses all segments of this code path then reports every
|
---|
188 | * invalid node.
|
---|
189 | * @param {CodePath} codePath A code path which was ended.
|
---|
190 | * @returns {void}
|
---|
191 | */
|
---|
192 | onCodePathEnd(codePath) {
|
---|
193 | const isDerivedClass = funcInfo.hasExtends;
|
---|
194 |
|
---|
195 | funcInfo = funcInfo.upper;
|
---|
196 | if (!isDerivedClass) {
|
---|
197 | return;
|
---|
198 | }
|
---|
199 |
|
---|
200 | codePath.traverseSegments((segment, controller) => {
|
---|
201 | const info = segInfoMap[segment.id];
|
---|
202 |
|
---|
203 | for (let i = 0; i < info.invalidNodes.length; ++i) {
|
---|
204 | const invalidNode = info.invalidNodes[i];
|
---|
205 |
|
---|
206 | context.report({
|
---|
207 | messageId: "noBeforeSuper",
|
---|
208 | node: invalidNode,
|
---|
209 | data: {
|
---|
210 | kind: invalidNode.type === "Super" ? "super" : "this"
|
---|
211 | }
|
---|
212 | });
|
---|
213 | }
|
---|
214 |
|
---|
215 | if (info.superCalled) {
|
---|
216 | controller.skip();
|
---|
217 | }
|
---|
218 | });
|
---|
219 | },
|
---|
220 |
|
---|
221 | /**
|
---|
222 | * Initialize information of a given code path segment.
|
---|
223 | * @param {CodePathSegment} segment A code path segment to initialize.
|
---|
224 | * @returns {void}
|
---|
225 | */
|
---|
226 | onCodePathSegmentStart(segment) {
|
---|
227 | funcInfo.currentSegments.add(segment);
|
---|
228 |
|
---|
229 | if (!isInConstructorOfDerivedClass()) {
|
---|
230 | return;
|
---|
231 | }
|
---|
232 |
|
---|
233 | // Initialize info.
|
---|
234 | segInfoMap[segment.id] = {
|
---|
235 | superCalled: (
|
---|
236 | segment.prevSegments.length > 0 &&
|
---|
237 | segment.prevSegments.every(isCalled)
|
---|
238 | ),
|
---|
239 | invalidNodes: []
|
---|
240 | };
|
---|
241 | },
|
---|
242 |
|
---|
243 | onUnreachableCodePathSegmentStart(segment) {
|
---|
244 | funcInfo.currentSegments.add(segment);
|
---|
245 | },
|
---|
246 |
|
---|
247 | onUnreachableCodePathSegmentEnd(segment) {
|
---|
248 | funcInfo.currentSegments.delete(segment);
|
---|
249 | },
|
---|
250 |
|
---|
251 | onCodePathSegmentEnd(segment) {
|
---|
252 | funcInfo.currentSegments.delete(segment);
|
---|
253 | },
|
---|
254 |
|
---|
255 | /**
|
---|
256 | * Update information of the code path segment when a code path was
|
---|
257 | * looped.
|
---|
258 | * @param {CodePathSegment} fromSegment The code path segment of the
|
---|
259 | * end of a loop.
|
---|
260 | * @param {CodePathSegment} toSegment A code path segment of the head
|
---|
261 | * of a loop.
|
---|
262 | * @returns {void}
|
---|
263 | */
|
---|
264 | onCodePathSegmentLoop(fromSegment, toSegment) {
|
---|
265 | if (!isInConstructorOfDerivedClass()) {
|
---|
266 | return;
|
---|
267 | }
|
---|
268 |
|
---|
269 | // Update information inside of the loop.
|
---|
270 | funcInfo.codePath.traverseSegments(
|
---|
271 | { first: toSegment, last: fromSegment },
|
---|
272 | (segment, controller) => {
|
---|
273 | const info = segInfoMap[segment.id];
|
---|
274 |
|
---|
275 | if (info.superCalled) {
|
---|
276 | info.invalidNodes = [];
|
---|
277 | controller.skip();
|
---|
278 | } else if (
|
---|
279 | segment.prevSegments.length > 0 &&
|
---|
280 | segment.prevSegments.every(isCalled)
|
---|
281 | ) {
|
---|
282 | info.superCalled = true;
|
---|
283 | info.invalidNodes = [];
|
---|
284 | }
|
---|
285 | }
|
---|
286 | );
|
---|
287 | },
|
---|
288 |
|
---|
289 | /**
|
---|
290 | * Reports if this is before `super()`.
|
---|
291 | * @param {ASTNode} node A target node.
|
---|
292 | * @returns {void}
|
---|
293 | */
|
---|
294 | ThisExpression(node) {
|
---|
295 | if (isBeforeCallOfSuper()) {
|
---|
296 | setInvalid(node);
|
---|
297 | }
|
---|
298 | },
|
---|
299 |
|
---|
300 | /**
|
---|
301 | * Reports if this is before `super()`.
|
---|
302 | * @param {ASTNode} node A target node.
|
---|
303 | * @returns {void}
|
---|
304 | */
|
---|
305 | Super(node) {
|
---|
306 | if (!astUtils.isCallee(node) && isBeforeCallOfSuper()) {
|
---|
307 | setInvalid(node);
|
---|
308 | }
|
---|
309 | },
|
---|
310 |
|
---|
311 | /**
|
---|
312 | * Marks `super()` called.
|
---|
313 | * @param {ASTNode} node A target node.
|
---|
314 | * @returns {void}
|
---|
315 | */
|
---|
316 | "CallExpression:exit"(node) {
|
---|
317 | if (node.callee.type === "Super" && isBeforeCallOfSuper()) {
|
---|
318 | setSuperCalled();
|
---|
319 | }
|
---|
320 | },
|
---|
321 |
|
---|
322 | /**
|
---|
323 | * Resets state.
|
---|
324 | * @returns {void}
|
---|
325 | */
|
---|
326 | "Program:exit"() {
|
---|
327 | segInfoMap = Object.create(null);
|
---|
328 | }
|
---|
329 | };
|
---|
330 | }
|
---|
331 | };
|
---|