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