| /** |
| * @fileoverview A rule to verify `super()` callings in constructor. |
| * @author Toru Nagashima |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Checks whether a given code path segment is reachable or not. |
| * |
| * @param {CodePathSegment} segment - A code path segment to check. |
| * @returns {boolean} `true` if the segment is reachable. |
| */ |
| function isReachable(segment) { |
| return segment.reachable; |
| } |
| |
| /** |
| * 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 "YieldExpression": |
| case "TaggedTemplateExpression": |
| case "MetaProperty": |
| return true; |
| |
| case "Identifier": |
| return node.name !== "undefined"; |
| |
| case "AssignmentExpression": |
| return isPossibleConstructor(node.right); |
| |
| case "LogicalExpression": |
| 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 |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "problem", |
| |
| docs: { |
| description: "require `super()` calls in constructors", |
| category: "ECMAScript 6", |
| recommended: true, |
| url: "https://eslint.org/docs/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 |
| }; |
| } else { |
| funcInfo = { |
| upper: funcInfo, |
| isConstructor: false, |
| hasExtends: false, |
| superIsConstructor: false, |
| codePath |
| }; |
| } |
| }, |
| |
| /** |
| * 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) { |
| 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); |
| } |
| }, |
| |
| /** |
| * 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.codePath.currentSegments; |
| let duplicate = false; |
| let info = null; |
| |
| for (let i = 0; i < segments.length; ++i) { |
| const segment = segments[i]; |
| |
| 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 (funcInfo.codePath.currentSegments.some(isReachable)) { |
| 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.codePath.currentSegments; |
| |
| for (let i = 0; i < segments.length; ++i) { |
| const segment = segments[i]; |
| |
| if (segment.reachable) { |
| const info = segInfoMap[segment.id]; |
| |
| info.calledInSomePaths = info.calledInEveryPaths = true; |
| } |
| } |
| }, |
| |
| /** |
| * Resets state. |
| * @returns {void} |
| */ |
| "Program:exit"() { |
| segInfoMap = Object.create(null); |
| } |
| }; |
| } |
| }; |