| /** | |
|  * @fileoverview A rule to disallow using `this`/`super` before `super()`. | |
|  * @author Toru Nagashima | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
|  | |
| const astUtils = require("./utils/ast-utils"); | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** | |
|  * 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" | |
|     ); | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Disallow `this`/`super` before calling `super()` in constructors", | |
|             recommended: true, | |
|             url: "https://eslint.org/docs/latest/rules/no-this-before-super" | |
|         }, | |
| 
 | |
|         schema: [], | |
| 
 | |
|         messages: { | |
|             noBeforeSuper: "'{{kind}}' is not allowed before 'super()'." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
| 
 | |
|         /* | |
|          * Information for each constructor. | |
|          * - upper:      Information of the upper constructor. | |
|          * - hasExtends: A flag which shows whether the owner class has a valid | |
|          *   `extends` part. | |
|          * - scope:      The scope of the owner class. | |
|          * - codePath:   The code path of this constructor. | |
|          */ | |
|         let funcInfo = null; | |
| 
 | |
|         /* | |
|          * Information for each code path segment. | |
|          * Each key is the id of a code path segment. | |
|          * Each value is an object: | |
|          * - superCalled:  The flag which shows `super()` called in all code paths. | |
|          * - invalidNodes: The array of invalid ThisExpression and Super nodes. | |
|          */ | |
|         let segInfoMap = Object.create(null); | |
| 
 | |
|         /** | |
|          * Gets whether or not `super()` is called in a given code path segment. | |
|          * @param {CodePathSegment} segment A code path segment to get. | |
|          * @returns {boolean} `true` if `super()` is called. | |
|          */ | |
|         function isCalled(segment) { | |
|             return !segment.reachable || segInfoMap[segment.id].superCalled; | |
|         } | |
| 
 | |
|         /** | |
|          * Checks whether or not this is in a constructor. | |
|          * @returns {boolean} `true` if this is in a constructor. | |
|          */ | |
|         function isInConstructorOfDerivedClass() { | |
|             return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends); | |
|         } | |
| 
 | |
|         /** | |
|          * Determines if every segment in a set has been called. | |
|          * @param {Set<CodePathSegment>} segments The segments to search. | |
|          * @returns {boolean} True if every segment has been called; false otherwise. | |
|          */ | |
|         function isEverySegmentCalled(segments) { | |
|             for (const segment of segments) { | |
|                 if (!isCalled(segment)) { | |
|                     return false; | |
|                 } | |
|             } | |
| 
 | |
|             return true; | |
|         } | |
| 
 | |
|         /** | |
|          * Checks whether or not this is before `super()` is called. | |
|          * @returns {boolean} `true` if this is before `super()` is called. | |
|          */ | |
|         function isBeforeCallOfSuper() { | |
|             return ( | |
|                 isInConstructorOfDerivedClass() && | |
|                 !isEverySegmentCalled(funcInfo.currentSegments) | |
|             ); | |
|         } | |
| 
 | |
|         /** | |
|          * Sets a given node as invalid. | |
|          * @param {ASTNode} node A node to set as invalid. This is one of | |
|          *      a ThisExpression and a Super. | |
|          * @returns {void} | |
|          */ | |
|         function setInvalid(node) { | |
|             const segments = funcInfo.currentSegments; | |
| 
 | |
|             for (const segment of segments) { | |
|                 if (segment.reachable) { | |
|                     segInfoMap[segment.id].invalidNodes.push(node); | |
|                 } | |
|             } | |
|         } | |
| 
 | |
|         /** | |
|          * Sets the current segment as `super` was called. | |
|          * @returns {void} | |
|          */ | |
|         function setSuperCalled() { | |
|             const segments = funcInfo.currentSegments; | |
| 
 | |
|             for (const segment of segments) { | |
|                 if (segment.reachable) { | |
|                     segInfoMap[segment.id].superCalled = true; | |
|                 } | |
|             } | |
|         } | |
| 
 | |
|         return { | |
| 
 | |
|             /** | |
|              * Adds information of a constructor into the stack. | |
|              * @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; | |
| 
 | |
|                     funcInfo = { | |
|                         upper: funcInfo, | |
|                         isConstructor: true, | |
|                         hasExtends: Boolean( | |
|                             classNode.superClass && | |
|                             !astUtils.isNullOrUndefined(classNode.superClass) | |
|                         ), | |
|                         codePath, | |
|                         currentSegments: new Set() | |
|                     }; | |
|                 } else { | |
|                     funcInfo = { | |
|                         upper: funcInfo, | |
|                         isConstructor: false, | |
|                         hasExtends: false, | |
|                         codePath, | |
|                         currentSegments: new Set() | |
|                     }; | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Removes the top of stack item. | |
|              * | |
|              * And this traverses all segments of this code path then reports every | |
|              * invalid node. | |
|              * @param {CodePath} codePath A code path which was ended. | |
|              * @returns {void} | |
|              */ | |
|             onCodePathEnd(codePath) { | |
|                 const isDerivedClass = funcInfo.hasExtends; | |
| 
 | |
|                 funcInfo = funcInfo.upper; | |
|                 if (!isDerivedClass) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 codePath.traverseSegments((segment, controller) => { | |
|                     const info = segInfoMap[segment.id]; | |
| 
 | |
|                     for (let i = 0; i < info.invalidNodes.length; ++i) { | |
|                         const invalidNode = info.invalidNodes[i]; | |
| 
 | |
|                         context.report({ | |
|                             messageId: "noBeforeSuper", | |
|                             node: invalidNode, | |
|                             data: { | |
|                                 kind: invalidNode.type === "Super" ? "super" : "this" | |
|                             } | |
|                         }); | |
|                     } | |
| 
 | |
|                     if (info.superCalled) { | |
|                         controller.skip(); | |
|                     } | |
|                 }); | |
|             }, | |
| 
 | |
|             /** | |
|              * 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 (!isInConstructorOfDerivedClass()) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Initialize info. | |
|                 segInfoMap[segment.id] = { | |
|                     superCalled: ( | |
|                         segment.prevSegments.length > 0 && | |
|                         segment.prevSegments.every(isCalled) | |
|                     ), | |
|                     invalidNodes: [] | |
|                 }; | |
|             }, | |
| 
 | |
|             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 (!isInConstructorOfDerivedClass()) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Update information inside of the loop. | |
|                 funcInfo.codePath.traverseSegments( | |
|                     { first: toSegment, last: fromSegment }, | |
|                     (segment, controller) => { | |
|                         const info = segInfoMap[segment.id]; | |
| 
 | |
|                         if (info.superCalled) { | |
|                             info.invalidNodes = []; | |
|                             controller.skip(); | |
|                         } else if ( | |
|                             segment.prevSegments.length > 0 && | |
|                             segment.prevSegments.every(isCalled) | |
|                         ) { | |
|                             info.superCalled = true; | |
|                             info.invalidNodes = []; | |
|                         } | |
|                     } | |
|                 ); | |
|             }, | |
| 
 | |
|             /** | |
|              * Reports if this is before `super()`. | |
|              * @param {ASTNode} node A target node. | |
|              * @returns {void} | |
|              */ | |
|             ThisExpression(node) { | |
|                 if (isBeforeCallOfSuper()) { | |
|                     setInvalid(node); | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Reports if this is before `super()`. | |
|              * @param {ASTNode} node A target node. | |
|              * @returns {void} | |
|              */ | |
|             Super(node) { | |
|                 if (!astUtils.isCallee(node) && isBeforeCallOfSuper()) { | |
|                     setInvalid(node); | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Marks `super()` called. | |
|              * @param {ASTNode} node A target node. | |
|              * @returns {void} | |
|              */ | |
|             "CallExpression:exit"(node) { | |
|                 if (node.callee.type === "Super" && isBeforeCallOfSuper()) { | |
|                     setSuperCalled(); | |
|                 } | |
|             }, | |
| 
 | |
|             /** | |
|              * Resets state. | |
|              * @returns {void} | |
|              */ | |
|             "Program:exit"() { | |
|                 segInfoMap = Object.create(null); | |
|             } | |
|         }; | |
|     } | |
| };
 |