-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: constructor-super
false positives with loops
#18226
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,22 +9,6 @@ | |
// Helpers | ||
//------------------------------------------------------------------------------ | ||
|
||
/** | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isAnySegmentReachable(segments) { | ||
|
||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Checks whether or not a given node is a constructor. | ||
* @param {ASTNode} node A node to check. This node type is one of | ||
|
@@ -165,8 +149,7 @@ module.exports = { | |
missingAll: "Expected to call 'super()'.", | ||
|
||
duplicate: "Unexpected duplicate 'super()'.", | ||
badSuper: "Unexpected 'super()' because 'super' is not a constructor.", | ||
unexpected: "Unexpected 'super()'." | ||
badSuper: "Unexpected 'super()' because 'super' is not a constructor." | ||
} | ||
}, | ||
|
||
|
@@ -186,15 +169,15 @@ module.exports = { | |
/** | ||
* @type {Record<string, SegmentInfo>} | ||
*/ | ||
let segInfoMap = Object.create(null); | ||
const 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; | ||
return segment.reachable && segInfoMap[segment.id].calledInSomePaths; | ||
} | ||
|
||
/** | ||
|
@@ -212,17 +195,6 @@ module.exports = { | |
* @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; | ||
} | ||
|
||
|
@@ -279,9 +251,9 @@ module.exports = { | |
} | ||
|
||
// Reports if `super()` lacked. | ||
const seenSegments = codePath.returnedSegments.filter(hasSegmentBeenSeen); | ||
const calledInEveryPaths = seenSegments.every(isCalledInEveryPath); | ||
const calledInSomePaths = seenSegments.some(isCalledInSomePath); | ||
const returnedSegments = codePath.returnedSegments; | ||
const calledInEveryPaths = returnedSegments.every(isCalledInEveryPath); | ||
const calledInSomePaths = returnedSegments.some(isCalledInSomePath); | ||
|
||
if (!calledInEveryPaths) { | ||
context.report({ | ||
|
@@ -296,28 +268,38 @@ module.exports = { | |
/** | ||
* Initialize information of a given code path segment. | ||
* @param {CodePathSegment} segment A code path segment to initialize. | ||
* @param {CodePathSegment} node Node that starts the segment. | ||
* @returns {void} | ||
*/ | ||
onCodePathSegmentStart(segment) { | ||
onCodePathSegmentStart(segment, node) { | ||
|
||
funcInfo.currentSegments.add(segment); | ||
|
||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
return; | ||
} | ||
|
||
// Initialize info. | ||
const info = segInfoMap[segment.id] = new SegmentInfo(); | ||
|
||
// When there are previous segments, aggregates these. | ||
const prevSegments = segment.prevSegments; | ||
|
||
if (prevSegments.length > 0) { | ||
const seenPrevSegments = prevSegments.filter(hasSegmentBeenSeen); | ||
const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen); | ||
|
||
// When there are previous segments, aggregates these. | ||
if (seenPrevSegments.length > 0) { | ||
info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath); | ||
info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath); | ||
} | ||
|
||
/* | ||
* ForStatement > *.update segments are a special case as they are created in advance, | ||
* without seen previous segments. Since they logically don't affect `calledInEveryPaths` | ||
* calculations, and they can never be a lone previous segment of another one, we'll set | ||
* their `calledInEveryPaths` to `true` to effectively ignore them in those calculations. | ||
* . | ||
*/ | ||
if (node.parent && node.parent.type === "ForStatement" && node.parent.update === node) { | ||
info.calledInEveryPaths = true; | ||
} | ||
}, | ||
|
||
onUnreachableCodePathSegmentStart(segment) { | ||
|
@@ -343,25 +325,30 @@ module.exports = { | |
* @returns {void} | ||
*/ | ||
onCodePathSegmentLoop(fromSegment, toSegment) { | ||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
if (!(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] ?? new SegmentInfo(); | ||
(segment, controller) => { | ||
const info = segInfoMap[segment.id]; | ||
|
||
// skip segments after the loop | ||
if (!info) { | ||
controller.skip(); | ||
return; | ||
} | ||
|
||
const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen); | ||
const calledInSomePreviousPaths = seenPrevSegments.some(isCalledInSomePath); | ||
const calledInEveryPreviousPaths = seenPrevSegments.every(isCalledInEveryPath); | ||
|
||
// Updates flags. | ||
info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath); | ||
info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath); | ||
info.calledInSomePaths ||= calledInSomePreviousPaths; | ||
info.calledInEveryPaths ||= calledInEveryPreviousPaths; | ||
|
||
// If flags become true anew, reports the valid nodes. | ||
if (info.calledInSomePaths || isRealLoop) { | ||
if (calledInSomePreviousPaths) { | ||
Comment on lines
343
to
+351
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code that updates segments in loops was causing false positives. It didn't account for the case that the segment could have had a |
||
const nodes = info.validNodes; | ||
|
||
info.validNodes = []; | ||
|
@@ -375,9 +362,6 @@ module.exports = { | |
}); | ||
} | ||
} | ||
|
||
// save just in case we created a new SegmentInfo object | ||
segInfoMap[segment.id] = info; | ||
} | ||
); | ||
}, | ||
|
@@ -388,7 +372,7 @@ module.exports = { | |
* @returns {void} | ||
*/ | ||
"CallExpression:exit"(node) { | ||
if (!(funcInfo && funcInfo.isConstructor)) { | ||
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
return; | ||
} | ||
|
||
|
@@ -398,41 +382,34 @@ module.exports = { | |
} | ||
|
||
// Reports if needed. | ||
if (funcInfo.hasExtends) { | ||
const segments = funcInfo.currentSegments; | ||
let duplicate = false; | ||
let info = null; | ||
const segments = funcInfo.currentSegments; | ||
let duplicate = false; | ||
let info = null; | ||
|
||
for (const segment of segments) { | ||
for (const segment of segments) { | ||
|
||
if (segment.reachable) { | ||
info = segInfoMap[segment.id]; | ||
if (segment.reachable) { | ||
info = segInfoMap[segment.id]; | ||
|
||
duplicate = duplicate || info.calledInSomePaths; | ||
info.calledInSomePaths = info.calledInEveryPaths = true; | ||
} | ||
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); | ||
} | ||
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 | ||
}); | ||
} | ||
Comment on lines
-431
to
436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was intended to report |
||
}, | ||
|
||
|
@@ -442,7 +419,7 @@ module.exports = { | |
* @returns {void} | ||
*/ | ||
ReturnStatement(node) { | ||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
return; | ||
} | ||
|
||
|
@@ -462,14 +439,6 @@ module.exports = { | |
info.calledInSomePaths = info.calledInEveryPaths = true; | ||
} | ||
} | ||
}, | ||
|
||
/** | ||
* Resets state. | ||
* @returns {void} | ||
*/ | ||
"Program:exit"() { | ||
segInfoMap = Object.create(null); | ||
} | ||
Comment on lines
-467
to
442
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there was any benefit from this. |
||
}; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no need to traverse segments after the loop here.