1 | /**
|
2 | * @fileoverview A rule to verify `super()` callings in constructor.
|
3 | * @author Toru Nagashima
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Helpers
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | /**
|
13 | * Checks whether a given code path segment is reachable or not.
|
14 | *
|
15 | * @param {CodePathSegment} segment - A code path segment to check.
|
16 | * @returns {boolean} `true` if the segment is reachable.
|
17 | */
|
18 | function isReachable(segment) {
|
19 | return segment.reachable;
|
20 | }
|
21 |
|
22 | /**
|
23 | * Checks whether or not a given node is a constructor.
|
24 | * @param {ASTNode} node - A node to check. This node type is one of
|
25 | * `Program`, `FunctionDeclaration`, `FunctionExpression`, and
|
26 | * `ArrowFunctionExpression`.
|
27 | * @returns {boolean} `true` if the node is a constructor.
|
28 | */
|
29 | function isConstructorFunction(node) {
|
30 | return (
|
31 | node.type === "FunctionExpression" &&
|
32 | node.parent.type === "MethodDefinition" &&
|
33 | node.parent.kind === "constructor"
|
34 | );
|
35 | }
|
36 |
|
37 | /**
|
38 | * Checks whether a given node can be a constructor or not.
|
39 | *
|
40 | * @param {ASTNode} node - A node to check.
|
41 | * @returns {boolean} `true` if the node can be a constructor.
|
42 | */
|
43 | function isPossibleConstructor(node) {
|
44 | if (!node) {
|
45 | return false;
|
46 | }
|
47 |
|
48 | switch (node.type) {
|
49 | case "ClassExpression":
|
50 | case "FunctionExpression":
|
51 | case "ThisExpression":
|
52 | case "MemberExpression":
|
53 | case "CallExpression":
|
54 | case "NewExpression":
|
55 | case "YieldExpression":
|
56 | case "TaggedTemplateExpression":
|
57 | case "MetaProperty":
|
58 | return true;
|
59 |
|
60 | case "Identifier":
|
61 | return node.name !== "undefined";
|
62 |
|
63 | case "AssignmentExpression":
|
64 | return isPossibleConstructor(node.right);
|
65 |
|
66 | case "LogicalExpression":
|
67 | return (
|
68 | isPossibleConstructor(node.left) ||
|
69 | isPossibleConstructor(node.right)
|
70 | );
|
71 |
|
72 | case "ConditionalExpression":
|
73 | return (
|
74 | isPossibleConstructor(node.alternate) ||
|
75 | isPossibleConstructor(node.consequent)
|
76 | );
|
77 |
|
78 | case "SequenceExpression": {
|
79 | const lastExpression = node.expressions[node.expressions.length - 1];
|
80 |
|
81 | return isPossibleConstructor(lastExpression);
|
82 | }
|
83 |
|
84 | default:
|
85 | return false;
|
86 | }
|
87 | }
|
88 |
|
89 | //------------------------------------------------------------------------------
|
90 | // Rule Definition
|
91 | //------------------------------------------------------------------------------
|
92 |
|
93 | module.exports = {
|
94 | meta: {
|
95 | type: "problem",
|
96 |
|
97 | docs: {
|
98 | description: "require `super()` calls in constructors",
|
99 | category: "ECMAScript 6",
|
100 | recommended: true,
|
101 | url: "https://eslint.org/docs/rules/constructor-super"
|
102 | },
|
103 |
|
104 | schema: [],
|
105 |
|
106 | messages: {
|
107 | missingSome: "Lacked a call of 'super()' in some code paths.",
|
108 | missingAll: "Expected to call 'super()'.",
|
109 |
|
110 | duplicate: "Unexpected duplicate 'super()'.",
|
111 | badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
|
112 | unexpected: "Unexpected 'super()'."
|
113 | }
|
114 | },
|
115 |
|
116 | create(context) {
|
117 |
|
118 | /*
|
119 | * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
|
120 | * Information for each constructor.
|
121 | * - upper: Information of the upper constructor.
|
122 | * - hasExtends: A flag which shows whether own class has a valid `extends`
|
123 | * part.
|
124 | * - scope: The scope of own class.
|
125 | * - codePath: The code path object of the constructor.
|
126 | */
|
127 | let funcInfo = null;
|
128 |
|
129 | /*
|
130 | * {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
|
131 | * Information for each code path segment.
|
132 | * - calledInSomePaths: A flag of be called `super()` in some code paths.
|
133 | * - calledInEveryPaths: A flag of be called `super()` in all code paths.
|
134 | * - validNodes:
|
135 | */
|
136 | let segInfoMap = Object.create(null);
|
137 |
|
138 | /**
|
139 | * Gets the flag which shows `super()` is called in some paths.
|
140 | * @param {CodePathSegment} segment - A code path segment to get.
|
141 | * @returns {boolean} The flag which shows `super()` is called in some paths
|
142 | */
|
143 | function isCalledInSomePath(segment) {
|
144 | return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
|
145 | }
|
146 |
|
147 | /**
|
148 | * Gets the flag which shows `super()` is called in all paths.
|
149 | * @param {CodePathSegment} segment - A code path segment to get.
|
150 | * @returns {boolean} The flag which shows `super()` is called in all paths.
|
151 | */
|
152 | function isCalledInEveryPath(segment) {
|
153 |
|
154 | /*
|
155 | * If specific segment is the looped segment of the current segment,
|
156 | * skip the segment.
|
157 | * If not skipped, this never becomes true after a loop.
|
158 | */
|
159 | if (segment.nextSegments.length === 1 &&
|
160 | segment.nextSegments[0].isLoopedPrevSegment(segment)
|
161 | ) {
|
162 | return true;
|
163 | }
|
164 | return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
|
165 | }
|
166 |
|
167 | return {
|
168 |
|
169 | /**
|
170 | * Stacks a constructor information.
|
171 | * @param {CodePath} codePath - A code path which was started.
|
172 | * @param {ASTNode} node - The current node.
|
173 | * @returns {void}
|
174 | */
|
175 | onCodePathStart(codePath, node) {
|
176 | if (isConstructorFunction(node)) {
|
177 |
|
178 | // Class > ClassBody > MethodDefinition > FunctionExpression
|
179 | const classNode = node.parent.parent.parent;
|
180 | const superClass = classNode.superClass;
|
181 |
|
182 | funcInfo = {
|
183 | upper: funcInfo,
|
184 | isConstructor: true,
|
185 | hasExtends: Boolean(superClass),
|
186 | superIsConstructor: isPossibleConstructor(superClass),
|
187 | codePath
|
188 | };
|
189 | } else {
|
190 | funcInfo = {
|
191 | upper: funcInfo,
|
192 | isConstructor: false,
|
193 | hasExtends: false,
|
194 | superIsConstructor: false,
|
195 | codePath
|
196 | };
|
197 | }
|
198 | },
|
199 |
|
200 | /**
|
201 | * Pops a constructor information.
|
202 | * And reports if `super()` lacked.
|
203 | * @param {CodePath} codePath - A code path which was ended.
|
204 | * @param {ASTNode} node - The current node.
|
205 | * @returns {void}
|
206 | */
|
207 | onCodePathEnd(codePath, node) {
|
208 | const hasExtends = funcInfo.hasExtends;
|
209 |
|
210 | // Pop.
|
211 | funcInfo = funcInfo.upper;
|
212 |
|
213 | if (!hasExtends) {
|
214 | return;
|
215 | }
|
216 |
|
217 | // Reports if `super()` lacked.
|
218 | const segments = codePath.returnedSegments;
|
219 | const calledInEveryPaths = segments.every(isCalledInEveryPath);
|
220 | const calledInSomePaths = segments.some(isCalledInSomePath);
|
221 |
|
222 | if (!calledInEveryPaths) {
|
223 | context.report({
|
224 | messageId: calledInSomePaths
|
225 | ? "missingSome"
|
226 | : "missingAll",
|
227 | node: node.parent
|
228 | });
|
229 | }
|
230 | },
|
231 |
|
232 | /**
|
233 | * Initialize information of a given code path segment.
|
234 | * @param {CodePathSegment} segment - A code path segment to initialize.
|
235 | * @returns {void}
|
236 | */
|
237 | onCodePathSegmentStart(segment) {
|
238 | if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
|
239 | return;
|
240 | }
|
241 |
|
242 | // Initialize info.
|
243 | const info = segInfoMap[segment.id] = {
|
244 | calledInSomePaths: false,
|
245 | calledInEveryPaths: false,
|
246 | validNodes: []
|
247 | };
|
248 |
|
249 | // When there are previous segments, aggregates these.
|
250 | const prevSegments = segment.prevSegments;
|
251 |
|
252 | if (prevSegments.length > 0) {
|
253 | info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
|
254 | info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
|
255 | }
|
256 | },
|
257 |
|
258 | /**
|
259 | * Update information of the code path segment when a code path was
|
260 | * looped.
|
261 | * @param {CodePathSegment} fromSegment - The code path segment of the
|
262 | * end of a loop.
|
263 | * @param {CodePathSegment} toSegment - A code path segment of the head
|
264 | * of a loop.
|
265 | * @returns {void}
|
266 | */
|
267 | onCodePathSegmentLoop(fromSegment, toSegment) {
|
268 | if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
|
269 | return;
|
270 | }
|
271 |
|
272 | // Update information inside of the loop.
|
273 | const isRealLoop = toSegment.prevSegments.length >= 2;
|
274 |
|
275 | funcInfo.codePath.traverseSegments(
|
276 | { first: toSegment, last: fromSegment },
|
277 | segment => {
|
278 | const info = segInfoMap[segment.id];
|
279 | const prevSegments = segment.prevSegments;
|
280 |
|
281 | // Updates flags.
|
282 | info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
|
283 | info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
|
284 |
|
285 | // If flags become true anew, reports the valid nodes.
|
286 | if (info.calledInSomePaths || isRealLoop) {
|
287 | const nodes = info.validNodes;
|
288 |
|
289 | info.validNodes = [];
|
290 |
|
291 | for (let i = 0; i < nodes.length; ++i) {
|
292 | const node = nodes[i];
|
293 |
|
294 | context.report({
|
295 | messageId: "duplicate",
|
296 | node
|
297 | });
|
298 | }
|
299 | }
|
300 | }
|
301 | );
|
302 | },
|
303 |
|
304 | /**
|
305 | * Checks for a call of `super()`.
|
306 | * @param {ASTNode} node - A CallExpression node to check.
|
307 | * @returns {void}
|
308 | */
|
309 | "CallExpression:exit"(node) {
|
310 | if (!(funcInfo && funcInfo.isConstructor)) {
|
311 | return;
|
312 | }
|
313 |
|
314 | // Skips except `super()`.
|
315 | if (node.callee.type !== "Super") {
|
316 | return;
|
317 | }
|
318 |
|
319 | // Reports if needed.
|
320 | if (funcInfo.hasExtends) {
|
321 | const segments = funcInfo.codePath.currentSegments;
|
322 | let duplicate = false;
|
323 | let info = null;
|
324 |
|
325 | for (let i = 0; i < segments.length; ++i) {
|
326 | const segment = segments[i];
|
327 |
|
328 | if (segment.reachable) {
|
329 | info = segInfoMap[segment.id];
|
330 |
|
331 | duplicate = duplicate || info.calledInSomePaths;
|
332 | info.calledInSomePaths = info.calledInEveryPaths = true;
|
333 | }
|
334 | }
|
335 |
|
336 | if (info) {
|
337 | if (duplicate) {
|
338 | context.report({
|
339 | messageId: "duplicate",
|
340 | node
|
341 | });
|
342 | } else if (!funcInfo.superIsConstructor) {
|
343 | context.report({
|
344 | messageId: "badSuper",
|
345 | node
|
346 | });
|
347 | } else {
|
348 | info.validNodes.push(node);
|
349 | }
|
350 | }
|
351 | } else if (funcInfo.codePath.currentSegments.some(isReachable)) {
|
352 | context.report({
|
353 | messageId: "unexpected",
|
354 | node
|
355 | });
|
356 | }
|
357 | },
|
358 |
|
359 | /**
|
360 | * Set the mark to the returned path as `super()` was called.
|
361 | * @param {ASTNode} node - A ReturnStatement node to check.
|
362 | * @returns {void}
|
363 | */
|
364 | ReturnStatement(node) {
|
365 | if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
|
366 | return;
|
367 | }
|
368 |
|
369 | // Skips if no argument.
|
370 | if (!node.argument) {
|
371 | return;
|
372 | }
|
373 |
|
374 | // Returning argument is a substitute of 'super()'.
|
375 | const segments = funcInfo.codePath.currentSegments;
|
376 |
|
377 | for (let i = 0; i < segments.length; ++i) {
|
378 | const segment = segments[i];
|
379 |
|
380 | if (segment.reachable) {
|
381 | const info = segInfoMap[segment.id];
|
382 |
|
383 | info.calledInSomePaths = info.calledInEveryPaths = true;
|
384 | }
|
385 | }
|
386 | },
|
387 |
|
388 | /**
|
389 | * Resets state.
|
390 | * @returns {void}
|
391 | */
|
392 | "Program:exit"() {
|
393 | segInfoMap = Object.create(null);
|
394 | }
|
395 | };
|
396 | }
|
397 | };
|