UNPKG

13.8 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to verify `super()` callings in constructor.
3 * @author Toru Nagashima
4 */
5
6"use strict";
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 */
18function 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 */
29function 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 */
43function 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
93module.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};