| |
| |
| |
| |
| |
| "use strict"; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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.at(-1); |
|
|
| return isPossibleConstructor(lastExpression); |
| } |
|
|
| default: |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| class SegmentInfo { |
| |
| |
| |
| |
| calledInEveryPaths = false; |
|
|
| |
| |
| |
| |
| calledInSomePaths = false; |
|
|
| |
| |
| |
| |
| validNodes = []; |
| } |
|
|
| |
| |
| |
|
|
| |
| 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.", |
| }, |
| }, |
|
|
| create(context) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let funcInfo = null; |
|
|
| |
| |
| |
| const segInfoMap = Object.create(null); |
|
|
| |
| |
| |
| |
| |
| function isCalledInSomePath(segment) { |
| return ( |
| segment.reachable && segInfoMap[segment.id].calledInSomePaths |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| function hasSegmentBeenSeen(segment) { |
| return !!segInfoMap[segment.id]; |
| } |
|
|
| |
| |
| |
| |
| |
| function isCalledInEveryPath(segment) { |
| 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 returnedSegments = codePath.returnedSegments; |
| const calledInEveryPaths = |
| returnedSegments.every(isCalledInEveryPath); |
| const calledInSomePaths = |
| returnedSegments.some(isCalledInSomePath); |
|
|
| if (!calledInEveryPaths) { |
| context.report({ |
| messageId: calledInSomePaths |
| ? "missingSome" |
| : "missingAll", |
| node: node.parent, |
| }); |
| } |
| }, |
|
|
| |
| |
| |
| |
| |
| |
| onCodePathSegmentStart(segment, node) { |
| funcInfo.currentSegments.add(segment); |
|
|
| if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { |
| return; |
| } |
|
|
| |
| const info = (segInfoMap[segment.id] = new SegmentInfo()); |
|
|
| const seenPrevSegments = |
| segment.prevSegments.filter(hasSegmentBeenSeen); |
|
|
| |
| if (seenPrevSegments.length > 0) { |
| info.calledInSomePaths = |
| seenPrevSegments.some(isCalledInSomePath); |
| info.calledInEveryPaths = |
| seenPrevSegments.every(isCalledInEveryPath); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if ( |
| node.parent && |
| node.parent.type === "ForStatement" && |
| node.parent.update === node |
| ) { |
| info.calledInEveryPaths = true; |
| } |
| }, |
|
|
| onUnreachableCodePathSegmentStart(segment) { |
| funcInfo.currentSegments.add(segment); |
| }, |
|
|
| onUnreachableCodePathSegmentEnd(segment) { |
| funcInfo.currentSegments.delete(segment); |
| }, |
|
|
| onCodePathSegmentEnd(segment) { |
| funcInfo.currentSegments.delete(segment); |
| }, |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| onCodePathSegmentLoop(fromSegment, toSegment) { |
| if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { |
| return; |
| } |
|
|
| funcInfo.codePath.traverseSegments( |
| { first: toSegment, last: fromSegment }, |
| (segment, controller) => { |
| const info = segInfoMap[segment.id]; |
|
|
| |
| if (!info) { |
| controller.skip(); |
| return; |
| } |
|
|
| const seenPrevSegments = |
| segment.prevSegments.filter(hasSegmentBeenSeen); |
| const calledInSomePreviousPaths = |
| seenPrevSegments.some(isCalledInSomePath); |
| const calledInEveryPreviousPaths = |
| seenPrevSegments.every(isCalledInEveryPath); |
|
|
| info.calledInSomePaths ||= calledInSomePreviousPaths; |
| info.calledInEveryPaths ||= calledInEveryPreviousPaths; |
|
|
| |
| if (calledInSomePreviousPaths) { |
| 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.isConstructor && funcInfo.hasExtends)) { |
| return; |
| } |
|
|
| |
| if (node.callee.type !== "Super") { |
| return; |
| } |
|
|
| |
| 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); |
| } |
| } |
| }, |
|
|
| |
| |
| |
| |
| |
| ReturnStatement(node) { |
| if (!(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; |
| } |
| } |
| }, |
| }; |
| }, |
| }; |
|
|