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