|
|
/** * @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 "ChainExpression": 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); } }; } };
|