1/** 2 * @fileoverview A rule to disallow using `this`/`super` before `super()`. 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18/** 19 * Checks whether or not a given node is a constructor. 20 * @param {ASTNode} node A node to check. This node type is one of 21 * `Program`, `FunctionDeclaration`, `FunctionExpression`, and 22 * `ArrowFunctionExpression`. 23 * @returns {boolean} `true` if the node is a constructor. 24 */ 25function isConstructorFunction(node) { 26 return ( 27 node.type === "FunctionExpression" && 28 node.parent.type === "MethodDefinition" && 29 node.parent.kind === "constructor" 30 ); 31} 32 33//------------------------------------------------------------------------------ 34// Rule Definition 35//------------------------------------------------------------------------------ 36 37module.exports = { 38 meta: { 39 type: "problem", 40 41 docs: { 42 description: "disallow `this`/`super` before calling `super()` in constructors", 43 category: "ECMAScript 6", 44 recommended: true, 45 url: "https://eslint.org/docs/rules/no-this-before-super" 46 }, 47 48 schema: [], 49 50 messages: { 51 noBeforeSuper: "'{{kind}}' is not allowed before 'super()'." 52 } 53 }, 54 55 create(context) { 56 57 /* 58 * Information for each constructor. 59 * - upper: Information of the upper constructor. 60 * - hasExtends: A flag which shows whether the owner class has a valid 61 * `extends` part. 62 * - scope: The scope of the owner class. 63 * - codePath: The code path of this constructor. 64 */ 65 let funcInfo = null; 66 67 /* 68 * Information for each code path segment. 69 * Each key is the id of a code path segment. 70 * Each value is an object: 71 * - superCalled: The flag which shows `super()` called in all code paths. 72 * - invalidNodes: The array of invalid ThisExpression and Super nodes. 73 */ 74 let segInfoMap = Object.create(null); 75 76 /** 77 * Gets whether or not `super()` is called in a given code path segment. 78 * @param {CodePathSegment} segment A code path segment to get. 79 * @returns {boolean} `true` if `super()` is called. 80 */ 81 function isCalled(segment) { 82 return !segment.reachable || segInfoMap[segment.id].superCalled; 83 } 84 85 /** 86 * Checks whether or not this is in a constructor. 87 * @returns {boolean} `true` if this is in a constructor. 88 */ 89 function isInConstructorOfDerivedClass() { 90 return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends); 91 } 92 93 /** 94 * Checks whether or not this is before `super()` is called. 95 * @returns {boolean} `true` if this is before `super()` is called. 96 */ 97 function isBeforeCallOfSuper() { 98 return ( 99 isInConstructorOfDerivedClass() && 100 !funcInfo.codePath.currentSegments.every(isCalled) 101 ); 102 } 103 104 /** 105 * Sets a given node as invalid. 106 * @param {ASTNode} node A node to set as invalid. This is one of 107 * a ThisExpression and a Super. 108 * @returns {void} 109 */ 110 function setInvalid(node) { 111 const segments = funcInfo.codePath.currentSegments; 112 113 for (let i = 0; i < segments.length; ++i) { 114 const segment = segments[i]; 115 116 if (segment.reachable) { 117 segInfoMap[segment.id].invalidNodes.push(node); 118 } 119 } 120 } 121 122 /** 123 * Sets the current segment as `super` was called. 124 * @returns {void} 125 */ 126 function setSuperCalled() { 127 const segments = funcInfo.codePath.currentSegments; 128 129 for (let i = 0; i < segments.length; ++i) { 130 const segment = segments[i]; 131 132 if (segment.reachable) { 133 segInfoMap[segment.id].superCalled = true; 134 } 135 } 136 } 137 138 return { 139 140 /** 141 * Adds information of a constructor into the stack. 142 * @param {CodePath} codePath A code path which was started. 143 * @param {ASTNode} node The current node. 144 * @returns {void} 145 */ 146 onCodePathStart(codePath, node) { 147 if (isConstructorFunction(node)) { 148 149 // Class > ClassBody > MethodDefinition > FunctionExpression 150 const classNode = node.parent.parent.parent; 151 152 funcInfo = { 153 upper: funcInfo, 154 isConstructor: true, 155 hasExtends: Boolean( 156 classNode.superClass && 157 !astUtils.isNullOrUndefined(classNode.superClass) 158 ), 159 codePath 160 }; 161 } else { 162 funcInfo = { 163 upper: funcInfo, 164 isConstructor: false, 165 hasExtends: false, 166 codePath 167 }; 168 } 169 }, 170 171 /** 172 * Removes the top of stack item. 173 * 174 * And this treverses all segments of this code path then reports every 175 * invalid node. 176 * @param {CodePath} codePath A code path which was ended. 177 * @returns {void} 178 */ 179 onCodePathEnd(codePath) { 180 const isDerivedClass = funcInfo.hasExtends; 181 182 funcInfo = funcInfo.upper; 183 if (!isDerivedClass) { 184 return; 185 } 186 187 codePath.traverseSegments((segment, controller) => { 188 const info = segInfoMap[segment.id]; 189 190 for (let i = 0; i < info.invalidNodes.length; ++i) { 191 const invalidNode = info.invalidNodes[i]; 192 193 context.report({ 194 messageId: "noBeforeSuper", 195 node: invalidNode, 196 data: { 197 kind: invalidNode.type === "Super" ? "super" : "this" 198 } 199 }); 200 } 201 202 if (info.superCalled) { 203 controller.skip(); 204 } 205 }); 206 }, 207 208 /** 209 * Initialize information of a given code path segment. 210 * @param {CodePathSegment} segment A code path segment to initialize. 211 * @returns {void} 212 */ 213 onCodePathSegmentStart(segment) { 214 if (!isInConstructorOfDerivedClass()) { 215 return; 216 } 217 218 // Initialize info. 219 segInfoMap[segment.id] = { 220 superCalled: ( 221 segment.prevSegments.length > 0 && 222 segment.prevSegments.every(isCalled) 223 ), 224 invalidNodes: [] 225 }; 226 }, 227 228 /** 229 * Update information of the code path segment when a code path was 230 * looped. 231 * @param {CodePathSegment} fromSegment The code path segment of the 232 * end of a loop. 233 * @param {CodePathSegment} toSegment A code path segment of the head 234 * of a loop. 235 * @returns {void} 236 */ 237 onCodePathSegmentLoop(fromSegment, toSegment) { 238 if (!isInConstructorOfDerivedClass()) { 239 return; 240 } 241 242 // Update information inside of the loop. 243 funcInfo.codePath.traverseSegments( 244 { first: toSegment, last: fromSegment }, 245 (segment, controller) => { 246 const info = segInfoMap[segment.id]; 247 248 if (info.superCalled) { 249 info.invalidNodes = []; 250 controller.skip(); 251 } else if ( 252 segment.prevSegments.length > 0 && 253 segment.prevSegments.every(isCalled) 254 ) { 255 info.superCalled = true; 256 info.invalidNodes = []; 257 } 258 } 259 ); 260 }, 261 262 /** 263 * Reports if this is before `super()`. 264 * @param {ASTNode} node A target node. 265 * @returns {void} 266 */ 267 ThisExpression(node) { 268 if (isBeforeCallOfSuper()) { 269 setInvalid(node); 270 } 271 }, 272 273 /** 274 * Reports if this is before `super()`. 275 * @param {ASTNode} node A target node. 276 * @returns {void} 277 */ 278 Super(node) { 279 if (!astUtils.isCallee(node) && isBeforeCallOfSuper()) { 280 setInvalid(node); 281 } 282 }, 283 284 /** 285 * Marks `super()` called. 286 * @param {ASTNode} node A target node. 287 * @returns {void} 288 */ 289 "CallExpression:exit"(node) { 290 if (node.callee.type === "Super" && isBeforeCallOfSuper()) { 291 setSuperCalled(); 292 } 293 }, 294 295 /** 296 * Resets state. 297 * @returns {void} 298 */ 299 "Program:exit"() { 300 segInfoMap = Object.create(null); 301 } 302 }; 303 } 304}; 305