Spaces:
Runtime error
Runtime error
/** | |
* @fileoverview A rule to verify `super()` callings in constructor. | |
* @author Toru Nagashima | |
*/ | |
; | |
//------------------------------------------------------------------------------ | |
// 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": | |
if (["=", "&&="].includes(node.operator)) { | |
return isPossibleConstructor(node.right); | |
} | |
if (["||=", "??="].includes(node.operator)) { | |
return ( | |
isPossibleConstructor(node.left) || | |
isPossibleConstructor(node.right) | |
); | |
} | |
/** | |
* All other assignment operators are mathematical assignment operators (arithmetic or bitwise). | |
* An assignment expression with a mathematical operator can either evaluate to a primitive value, | |
* or throw, depending on the operands. Thus, it cannot evaluate to a constructor function. | |
*/ | |
return false; | |
case "LogicalExpression": | |
/* | |
* If the && operator short-circuits, the left side was falsy and therefore not a constructor, and if | |
* it doesn't short-circuit, it takes the value from the right side, so the right side must always be a | |
* possible constructor. A future improvement could verify that the left side could be truthy by | |
* excluding falsy literals. | |
*/ | |
if (node.operator === "&&") { | |
return isPossibleConstructor(node.right); | |
} | |
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); | |
} | |
}; | |
} | |
}; | |