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 docs: {
96 description: "require `super()` calls in constructors",
97 category: "ECMAScript 6",
98 recommended: true,
99 url: "https://eslint.org/docs/rules/constructor-super"
100 },
101
102 schema: [],
103
104 messages: {
105 missingSome: "Lacked a call of 'super()' in some code paths.",
106 missingAll: "Expected to call 'super()'.",
107
108 duplicate: "Unexpected duplicate 'super()'.",
109 badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
110 unexpected: "Unexpected 'super()'."
111 }
112 },
113
114 create(context) {
115
116 /*
117 * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
118 * Information for each constructor.
119 * - upper: Information of the upper constructor.
120 * - hasExtends: A flag which shows whether own class has a valid `extends`
121 * part.
122 * - scope: The scope of own class.
123 * - codePath: The code path object of the constructor.
124 */
125 let funcInfo = null;
126
127 /*
128 * {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
129 * Information for each code path segment.
130 * - calledInSomePaths: A flag of be called `super()` in some code paths.
131 * - calledInEveryPaths: A flag of be called `super()` in all code paths.
132 * - validNodes:
133 */
134 let segInfoMap = Object.create(null);
135
136 /**
137 * Gets the flag which shows `super()` is called in some paths.
138 * @param {CodePathSegment} segment - A code path segment to get.
139 * @returns {boolean} The flag which shows `super()` is called in some paths
140 */
141 function isCalledInSomePath(segment) {
142 return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
143 }
144
145 /**
146 * Gets the flag which shows `super()` is called in all paths.
147 * @param {CodePathSegment} segment - A code path segment to get.
148 * @returns {boolean} The flag which shows `super()` is called in all paths.
149 */
150 function isCalledInEveryPath(segment) {
151
152 /*
153 * If specific segment is the looped segment of the current segment,
154 * skip the segment.
155 * If not skipped, this never becomes true after a loop.
156 */
157 if (segment.nextSegments.length === 1 &&
158 segment.nextSegments[0].isLoopedPrevSegment(segment)
159 ) {
160 return true;
161 }
162 return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
163 }
164
165 return {
166
167 /**
168 * Stacks a constructor information.
169 * @param {CodePath} codePath - A code path which was started.
170 * @param {ASTNode} node - The current node.
171 * @returns {void}
172 */
173 onCodePathStart(codePath, node) {
174 if (isConstructorFunction(node)) {
175
176 // Class > ClassBody > MethodDefinition > FunctionExpression
177 const classNode = node.parent.parent.parent;
178 const superClass = classNode.superClass;
179
180 funcInfo = {
181 upper: funcInfo,
182 isConstructor: true,
183 hasExtends: Boolean(superClass),
184 superIsConstructor: isPossibleConstructor(superClass),
185 codePath
186 };
187 } else {
188 funcInfo = {
189 upper: funcInfo,
190 isConstructor: false,
191 hasExtends: false,
192 superIsConstructor: false,
193 codePath
194 };
195 }
196 },
197
198 /**
199 * Pops a constructor information.
200 * And reports if `super()` lacked.
201 * @param {CodePath} codePath - A code path which was ended.
202 * @param {ASTNode} node - The current node.
203 * @returns {void}
204 */
205 onCodePathEnd(codePath, node) {
206 const hasExtends = funcInfo.hasExtends;
207
208 // Pop.
209 funcInfo = funcInfo.upper;
210
211 if (!hasExtends) {
212 return;
213 }
214
215 // Reports if `super()` lacked.
216 const segments = codePath.returnedSegments;
217 const calledInEveryPaths = segments.every(isCalledInEveryPath);
218 const calledInSomePaths = segments.some(isCalledInSomePath);
219
220 if (!calledInEveryPaths) {
221 context.report({
222 messageId: calledInSomePaths
223 ? "missingSome"
224 : "missingAll",
225 node: node.parent
226 });
227 }
228 },
229
230 /**
231 * Initialize information of a given code path segment.
232 * @param {CodePathSegment} segment - A code path segment to initialize.
233 * @returns {void}
234 */
235 onCodePathSegmentStart(segment) {
236 if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
237 return;
238 }
239
240 // Initialize info.
241 const info = segInfoMap[segment.id] = {
242 calledInSomePaths: false,
243 calledInEveryPaths: false,
244 validNodes: []
245 };
246
247 // When there are previous segments, aggregates these.
248 const prevSegments = segment.prevSegments;
249
250 if (prevSegments.length > 0) {
251 info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
252 info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
253 }
254 },
255
256 /**
257 * Update information of the code path segment when a code path was
258 * looped.
259 * @param {CodePathSegment} fromSegment - The code path segment of the
260 * end of a loop.
261 * @param {CodePathSegment} toSegment - A code path segment of the head
262 * of a loop.
263 * @returns {void}
264 */
265 onCodePathSegmentLoop(fromSegment, toSegment) {
266 if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
267 return;
268 }
269
270 // Update information inside of the loop.
271 const isRealLoop = toSegment.prevSegments.length >= 2;
272
273 funcInfo.codePath.traverseSegments(
274 { first: toSegment, last: fromSegment },
275 segment => {
276 const info = segInfoMap[segment.id];
277 const prevSegments = segment.prevSegments;
278
279 // Updates flags.
280 info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
281 info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
282
283 // If flags become true anew, reports the valid nodes.
284 if (info.calledInSomePaths || isRealLoop) {
285 const nodes = info.validNodes;
286
287 info.validNodes = [];
288
289 for (let i = 0; i < nodes.length; ++i) {
290 const node = nodes[i];
291
292 context.report({
293 messageId: "duplicate",
294 node
295 });
296 }
297 }
298 }
299 );
300 },
301
302 /**
303 * Checks for a call of `super()`.
304 * @param {ASTNode} node - A CallExpression node to check.
305 * @returns {void}
306 */
307 "CallExpression:exit"(node) {
308 if (!(funcInfo && funcInfo.isConstructor)) {
309 return;
310 }
311
312 // Skips except `super()`.
313 if (node.callee.type !== "Super") {
314 return;
315 }
316
317 // Reports if needed.
318 if (funcInfo.hasExtends) {
319 const segments = funcInfo.codePath.currentSegments;
320 let duplicate = false;
321 let info = null;
322
323 for (let i = 0; i < segments.length; ++i) {
324 const segment = segments[i];
325
326 if (segment.reachable) {
327 info = segInfoMap[segment.id];
328
329 duplicate = duplicate || info.calledInSomePaths;
330 info.calledInSomePaths = info.calledInEveryPaths = true;
331 }
332 }
333
334 if (info) {
335 if (duplicate) {
336 context.report({
337 messageId: "duplicate",
338 node
339 });
340 } else if (!funcInfo.superIsConstructor) {
341 context.report({
342 messageId: "badSuper",
343 node
344 });
345 } else {
346 info.validNodes.push(node);
347 }
348 }
349 } else if (funcInfo.codePath.currentSegments.some(isReachable)) {
350 context.report({
351 messageId: "unexpected",
352 node
353 });
354 }
355 },
356
357 /**
358 * Set the mark to the returned path as `super()` was called.
359 * @param {ASTNode} node - A ReturnStatement node to check.
360 * @returns {void}
361 */
362 ReturnStatement(node) {
363 if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
364 return;
365 }
366
367 // Skips if no argument.
368 if (!node.argument) {
369 return;
370 }
371
372 // Returning argument is a substitute of 'super()'.
373 const segments = funcInfo.codePath.currentSegments;
374
375 for (let i = 0; i < segments.length; ++i) {
376 const segment = segments[i];
377
378 if (segment.reachable) {
379 const info = segInfoMap[segment.id];
380
381 info.calledInSomePaths = info.calledInEveryPaths = true;
382 }
383 }
384 },
385
386 /**
387 * Resets state.
388 * @returns {void}
389 */
390 "Program:exit"() {
391 segInfoMap = Object.create(null);
392 }
393 };
394 }
395};