|
|
|
|
|
|
|
|
|
|
|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isAnySegmentReachable(segments) { |
|
|
|
for (const segment of segments) { |
|
if (segment.reachable) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isConstructorFunction(node) { |
|
return ( |
|
node.type === "FunctionExpression" && |
|
node.parent.type === "MethodDefinition" && |
|
node.parent.kind === "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) |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
case "LogicalExpression": |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = { |
|
meta: { |
|
type: "problem", |
|
|
|
docs: { |
|
description: "Require `super()` calls in constructors", |
|
recommended: true, |
|
url: "https://eslint.org/docs/latest/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) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let funcInfo = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let segInfoMap = Object.create(null); |
|
|
|
|
|
|
|
|
|
|
|
|
|
function isCalledInSomePath(segment) { |
|
return segment.reachable && segInfoMap[segment.id].calledInSomePaths; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function isCalledInEveryPath(segment) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (segment.nextSegments.length === 1 && |
|
segment.nextSegments[0].isLoopedPrevSegment(segment) |
|
) { |
|
return true; |
|
} |
|
return segment.reachable && segInfoMap[segment.id].calledInEveryPaths; |
|
} |
|
|
|
return { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onCodePathStart(codePath, node) { |
|
if (isConstructorFunction(node)) { |
|
|
|
|
|
const classNode = node.parent.parent.parent; |
|
const superClass = classNode.superClass; |
|
|
|
funcInfo = { |
|
upper: funcInfo, |
|
isConstructor: true, |
|
hasExtends: Boolean(superClass), |
|
superIsConstructor: isPossibleConstructor(superClass), |
|
codePath, |
|
currentSegments: new Set() |
|
}; |
|
} else { |
|
funcInfo = { |
|
upper: funcInfo, |
|
isConstructor: false, |
|
hasExtends: false, |
|
superIsConstructor: false, |
|
codePath, |
|
currentSegments: new Set() |
|
}; |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onCodePathEnd(codePath, node) { |
|
const hasExtends = funcInfo.hasExtends; |
|
|
|
|
|
funcInfo = funcInfo.upper; |
|
|
|
if (!hasExtends) { |
|
return; |
|
} |
|
|
|
|
|
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 |
|
}); |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
onCodePathSegmentStart(segment) { |
|
|
|
funcInfo.currentSegments.add(segment); |
|
|
|
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { |
|
return; |
|
} |
|
|
|
|
|
const info = segInfoMap[segment.id] = { |
|
calledInSomePaths: false, |
|
calledInEveryPaths: false, |
|
validNodes: [] |
|
}; |
|
|
|
|
|
const prevSegments = segment.prevSegments; |
|
|
|
if (prevSegments.length > 0) { |
|
info.calledInSomePaths = prevSegments.some(isCalledInSomePath); |
|
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath); |
|
} |
|
}, |
|
|
|
onUnreachableCodePathSegmentStart(segment) { |
|
funcInfo.currentSegments.add(segment); |
|
}, |
|
|
|
onUnreachableCodePathSegmentEnd(segment) { |
|
funcInfo.currentSegments.delete(segment); |
|
}, |
|
|
|
onCodePathSegmentEnd(segment) { |
|
funcInfo.currentSegments.delete(segment); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onCodePathSegmentLoop(fromSegment, toSegment) { |
|
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { |
|
return; |
|
} |
|
|
|
|
|
const isRealLoop = toSegment.prevSegments.length >= 2; |
|
|
|
funcInfo.codePath.traverseSegments( |
|
{ first: toSegment, last: fromSegment }, |
|
segment => { |
|
const info = segInfoMap[segment.id]; |
|
const prevSegments = segment.prevSegments; |
|
|
|
|
|
info.calledInSomePaths = prevSegments.some(isCalledInSomePath); |
|
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath); |
|
|
|
|
|
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 |
|
}); |
|
} |
|
} |
|
} |
|
); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
"CallExpression:exit"(node) { |
|
if (!(funcInfo && funcInfo.isConstructor)) { |
|
return; |
|
} |
|
|
|
|
|
if (node.callee.type !== "Super") { |
|
return; |
|
} |
|
|
|
|
|
if (funcInfo.hasExtends) { |
|
const segments = funcInfo.currentSegments; |
|
let duplicate = false; |
|
let info = null; |
|
|
|
for (const segment of segments) { |
|
|
|
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 (isAnySegmentReachable(funcInfo.currentSegments)) { |
|
context.report({ |
|
messageId: "unexpected", |
|
node |
|
}); |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
ReturnStatement(node) { |
|
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { |
|
return; |
|
} |
|
|
|
|
|
if (!node.argument) { |
|
return; |
|
} |
|
|
|
|
|
const segments = funcInfo.currentSegments; |
|
|
|
for (const segment of segments) { |
|
|
|
if (segment.reachable) { |
|
const info = segInfoMap[segment.id]; |
|
|
|
info.calledInSomePaths = info.calledInEveryPaths = true; |
|
} |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
"Program:exit"() { |
|
segInfoMap = Object.create(null); |
|
} |
|
}; |
|
} |
|
}; |
|
|