| /** | |
|  * @fileoverview A rule to verify `super()` callings in constructor. | |
|  * @author Toru Nagashima | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** | |
|  * Checks all segments in a set and returns true if any are reachable. | |
|  * @param {Set<CodePathSegment>} segments The segments to check. | |
|  * @returns {boolean} True if any segment is reachable; false otherwise. | |
|  */ | |
| function isAnySegmentReachable(segments) { | |
| 
 | |
|     for (const segment of segments) { | |
|         if (segment.reachable) { | |
|             return true; | |
|         } | |
|     } | |
| 
 | |
|     return false; | |
| } | |
| 
 | |
| /** | |
|  * Checks whether or not a given node is a constructor. | |
|  * @param {ASTNode} node A node to check. This node type is one of | |
|  *   `Program`, `FunctionDeclaration`, `FunctionExpression`, and | |
|  *   `ArrowFunctionExpression`. | |
|  * @returns {boolean} `true` if the node is a constructor. | |
|  */ | |
| function isConstructorFunction(node) { | |
|     return ( | |
|         node.type === "FunctionExpression" && | |
|         node.parent.type === "MethodDefinition" && | |
|         node.parent.kind === "constructor" | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Checks whether a given node can be a constructor or not. | |
|  * @param {ASTNode} node A node to check. | |
|  * @returns {boolean} `true` if the node can be a constructor. | |
|  */ | |
| function isPossibleConstructor(node) { | |
|     if (!node) { | |
|         return false; | |
|     } | |
| 
 | |
|     switch (node.type) { | |
|         case "ClassExpression": | |
|         case "FunctionExpression": | |
|         case "ThisExpression": | |
|         case "MemberExpression": | |
|         case "CallExpression": | |
|         case "NewExpression": | |
|         case "ChainExpression": | |
|         case "YieldExpression": | |
|         case "TaggedTemplateExpression": | |
|         case "MetaProperty": | |
|             return true; | |
| 
 | |
|         case "Identifier": | |
|             return node.name !== "undefined"; | |
| 
 | |
|         case "AssignmentExpression": | |
|             if (["=", "&&="].includes(node.operator)) { | |
|                 return isPossibleConstructor(node.right); | |
|             } | |
| 
 | |
|             if (["||=", "??="].includes(node.operator)) { | |
|                 return ( | |
|                     isPossibleConstructor(node.left) || | |
|                     isPossibleConstructor(node.right) | |
|                 ); | |
|             } | |
| 
 | |
|             /** | |
|              * All other assignment operators are mathematical assignment operators (arithmetic or bitwise). | |
|              * An assignment expression with a mathematical operator can either evaluate to a primitive value, | |
|              * or throw, depending on the operands. Thus, it cannot evaluate to a constructor function. | |
|              */ | |
|             return false; | |
| 
 | |
|         case "LogicalExpression": | |
| 
 | |
|             /* | |
|              * If the && operator short-circuits, the left side was falsy and therefore not a constructor, and if | |
|              * it doesn't short-circuit, it takes the value from the right side, so the right side must always be a | |
|              * possible constructor. A future improvement could verify that the left side could be truthy by | |
|              * excluding falsy literals. | |
|              */ | |
|             if (node.operator === "&&") { | |
|                 return isPossibleConstructor(node.right); | |
|             } | |
| 
 | |
|             return ( | |
|                 isPossibleConstructor(node.left) || | |
|                 isPossibleConstructor(node.right) | |
|             ); | |
| 
 | |
|         case "ConditionalExpression": | |
|             return ( | |
|                 isPossibleConstructor(node.alternate) || | |
|                 isPossibleConstructor(node.consequent) | |
|             ); | |
| 
 | |
|         case "SequenceExpression": { | |
|             const lastExpression = node.expressions[node.expressions.length - 1]; | |
| 
 | |
|             return isPossibleConstructor(lastExpression); | |
|         } | |
| 
 | |
|         default: | |
|             return false; | |
|     } | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Require `super()` calls in constructors", | |
|             recommended: true, | |
|             url: "https://eslint.org/docs/latest/rules/constructor-super" | |
|         }, | |
| 
 | |
|         schema: [], | |
| 
 | |
|         messages: { | |
|             missingSome: "Lacked a call of 'super()' in some code paths.", | |
|             missingAll: "Expected to call 'super()'.", | |
| 
 | |
|             duplicate: "Unexpected duplicate 'super()'.", | |
|             badSuper: "Unexpected 'super()' because 'super' is not a constructor.", | |
|             unexpected: "Unexpected 'super()'." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
| 
 | |
|         /* | |
|          * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]} | |
|          * Information for each constructor. | |
|          * - upper:      Information of the upper constructor. | |
|          * - hasExtends: A flag which shows whether own class has a valid `extends` | |
|          *               part. | |
|          * - scope:      The scope of own class. | |
|          * - codePath:   The code path object of the constructor. | |
|          */ | |
|         let funcInfo = null; | |
| 
 | |
|         /* | |
|          * {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>} | |
|          * Information for each code path segment. | |
|          * - calledInSomePaths:  A flag of be called `super()` in some code paths. | |
|          * - calledInEveryPaths: A flag of be called `super()` in all code paths. | |
|          * - validNodes: | |
|          */ | |
|         let segInfoMap = Object.create(null); | |
| 
 | |
|         /** | |
|          * Gets the flag which shows `super()` is called in some paths. | |
|          * @param {CodePathSegment} segment A code path segment to get. | |
|          * @returns {boolean} The flag which shows `super()` is called in some paths | |
|          */ | |
|         function isCalledInSomePath(segment) { | |
|             return segment.reachable && segInfoMap[segment.id].calledInSomePaths; | |
|         } | |
| 
 | |
|         /** | |
|          * Gets the flag which shows `super()` is called in all paths. | |
|          * @param {CodePathSegment} segment A code path segment to get. | |
|          * @returns {boolean} The flag which shows `super()` is called in all paths. | |
|          */ | |
|         function isCalledInEveryPath(segment) { | |
| 
 | |
|             /* | |
|              * If specific segment is the looped segment of the current segment, | |
|              * skip the segment. | |
|              * If not skipped, this never becomes true after a loop. | |
|              */ | |
|             if (segment.nextSegments.length === 1 && | |
|                 segment.nextSegments[0].isLoopedPrevSegment(segment) | |
|             ) { | |
|                 return true; | |
|             } | |
|             return segment.reachable && segInfoMap[segment.id].calledInEveryPaths; | |
|         } | |
| 
 | |
|         return { | |
| 
 | |
|             /** | |
|              * Stacks a constructor information. | |
|              * @param {CodePath} codePath A code path which was started. | |
|              * @param {ASTNode} node The current node. | |
|              * @returns {void} | |
|              */ | |
|             onCodePathStart(codePath, node) { | |
|                 if (isConstructorFunction(node)) { | |
| 
 | |
|                     // Class > ClassBody > MethodDefinition > FunctionExpression | |
|                     const classNode = node.parent.parent.parent; | |
|                     const superClass = classNode.superClass; | |
| 
 | |
|                     funcInfo = { | |
|                         upper: funcInfo, | |
|                         isConstructor: true, | |
|                         hasExtends: Boolean(superClass), | |
|                         superIsConstructor: isPossibleConstructor(superClass), | |
|                         codePath, | |
|                         currentSegments: new Set() | |
|                     }; | |
|                 } else { | |
|                     funcInfo = { | |
|                         upper: funcInfo, | |
|                         isConstructor: false, | |
|                         hasExtends: false, | |
|                         superIsConstructor: false, | |
|                         codePath, | |
|                         currentSegments: new Set() | |
|                     }; | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Pops a constructor information. | |
|              * And reports if `super()` lacked. | |
|              * @param {CodePath} codePath A code path which was ended. | |
|              * @param {ASTNode} node The current node. | |
|              * @returns {void} | |
|              */ | |
|             onCodePathEnd(codePath, node) { | |
|                 const hasExtends = funcInfo.hasExtends; | |
| 
 | |
|                 // Pop. | |
|                 funcInfo = funcInfo.upper; | |
| 
 | |
|                 if (!hasExtends) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Reports if `super()` lacked. | |
|                 const segments = codePath.returnedSegments; | |
|                 const calledInEveryPaths = segments.every(isCalledInEveryPath); | |
|                 const calledInSomePaths = segments.some(isCalledInSomePath); | |
| 
 | |
|                 if (!calledInEveryPaths) { | |
|                     context.report({ | |
|                         messageId: calledInSomePaths | |
|                             ? "missingSome" | |
|                             : "missingAll", | |
|                         node: node.parent | |
|                     }); | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Initialize information of a given code path segment. | |
|              * @param {CodePathSegment} segment A code path segment to initialize. | |
|              * @returns {void} | |
|              */ | |
|             onCodePathSegmentStart(segment) { | |
| 
 | |
|                 funcInfo.currentSegments.add(segment); | |
| 
 | |
|                 if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Initialize info. | |
|                 const info = segInfoMap[segment.id] = { | |
|                     calledInSomePaths: false, | |
|                     calledInEveryPaths: false, | |
|                     validNodes: [] | |
|                 }; | |
| 
 | |
|                 // When there are previous segments, aggregates these. | |
|                 const prevSegments = segment.prevSegments; | |
| 
 | |
|                 if (prevSegments.length > 0) { | |
|                     info.calledInSomePaths = prevSegments.some(isCalledInSomePath); | |
|                     info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath); | |
|                 } | |
|             }, | |
| 
 | |
|             onUnreachableCodePathSegmentStart(segment) { | |
|                 funcInfo.currentSegments.add(segment); | |
|             }, | |
| 
 | |
|             onUnreachableCodePathSegmentEnd(segment) { | |
|                 funcInfo.currentSegments.delete(segment); | |
|             }, | |
| 
 | |
|             onCodePathSegmentEnd(segment) { | |
|                 funcInfo.currentSegments.delete(segment); | |
|             }, | |
| 
 | |
| 
 | |
|             /** | |
|              * Update information of the code path segment when a code path was | |
|              * looped. | |
|              * @param {CodePathSegment} fromSegment The code path segment of the | |
|              *      end of a loop. | |
|              * @param {CodePathSegment} toSegment A code path segment of the head | |
|              *      of a loop. | |
|              * @returns {void} | |
|              */ | |
|             onCodePathSegmentLoop(fromSegment, toSegment) { | |
|                 if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Update information inside of the loop. | |
|                 const isRealLoop = toSegment.prevSegments.length >= 2; | |
| 
 | |
|                 funcInfo.codePath.traverseSegments( | |
|                     { first: toSegment, last: fromSegment }, | |
|                     segment => { | |
|                         const info = segInfoMap[segment.id]; | |
|                         const prevSegments = segment.prevSegments; | |
| 
 | |
|                         // Updates flags. | |
|                         info.calledInSomePaths = prevSegments.some(isCalledInSomePath); | |
|                         info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath); | |
| 
 | |
|                         // If flags become true anew, reports the valid nodes. | |
|                         if (info.calledInSomePaths || isRealLoop) { | |
|                             const nodes = info.validNodes; | |
| 
 | |
|                             info.validNodes = []; | |
| 
 | |
|                             for (let i = 0; i < nodes.length; ++i) { | |
|                                 const node = nodes[i]; | |
| 
 | |
|                                 context.report({ | |
|                                     messageId: "duplicate", | |
|                                     node | |
|                                 }); | |
|                             } | |
|                         } | |
|                     } | |
|                 ); | |
|             }, | |
| 
 | |
|             /** | |
|              * Checks for a call of `super()`. | |
|              * @param {ASTNode} node A CallExpression node to check. | |
|              * @returns {void} | |
|              */ | |
|             "CallExpression:exit"(node) { | |
|                 if (!(funcInfo && funcInfo.isConstructor)) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Skips except `super()`. | |
|                 if (node.callee.type !== "Super") { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Reports if needed. | |
|                 if (funcInfo.hasExtends) { | |
|                     const segments = funcInfo.currentSegments; | |
|                     let duplicate = false; | |
|                     let info = null; | |
| 
 | |
|                     for (const segment of segments) { | |
| 
 | |
|                         if (segment.reachable) { | |
|                             info = segInfoMap[segment.id]; | |
| 
 | |
|                             duplicate = duplicate || info.calledInSomePaths; | |
|                             info.calledInSomePaths = info.calledInEveryPaths = true; | |
|                         } | |
|                     } | |
| 
 | |
|                     if (info) { | |
|                         if (duplicate) { | |
|                             context.report({ | |
|                                 messageId: "duplicate", | |
|                                 node | |
|                             }); | |
|                         } else if (!funcInfo.superIsConstructor) { | |
|                             context.report({ | |
|                                 messageId: "badSuper", | |
|                                 node | |
|                             }); | |
|                         } else { | |
|                             info.validNodes.push(node); | |
|                         } | |
|                     } | |
|                 } else if (isAnySegmentReachable(funcInfo.currentSegments)) { | |
|                     context.report({ | |
|                         messageId: "unexpected", | |
|                         node | |
|                     }); | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Set the mark to the returned path as `super()` was called. | |
|              * @param {ASTNode} node A ReturnStatement node to check. | |
|              * @returns {void} | |
|              */ | |
|             ReturnStatement(node) { | |
|                 if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Skips if no argument. | |
|                 if (!node.argument) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Returning argument is a substitute of 'super()'. | |
|                 const segments = funcInfo.currentSegments; | |
| 
 | |
|                 for (const segment of segments) { | |
| 
 | |
|                     if (segment.reachable) { | |
|                         const info = segInfoMap[segment.id]; | |
| 
 | |
|                         info.calledInSomePaths = info.calledInEveryPaths = true; | |
|                     } | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Resets state. | |
|              * @returns {void} | |
|              */ | |
|             "Program:exit"() { | |
|                 segInfoMap = Object.create(null); | |
|             } | |
|         }; | |
|     } | |
| };
 |