Skip to content
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

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
155 changes: 62 additions & 93 deletions lib/rules/constructor-super.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."
}
},

Expand All @@ -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;
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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({
Expand All @@ -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) {
Expand All @@ -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;
}
Comment on lines -355 to +341
Copy link
Member Author

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.


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
Copy link
Member Author

Choose a reason for hiding this comment

The 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 super() call inside the segment itself.

const nodes = info.validNodes;

info.validNodes = [];
Expand All @@ -375,9 +362,6 @@ module.exports = {
});
}
}

// save just in case we created a new SegmentInfo object
segInfoMap[segment.id] = info;
}
);
},
Expand All @@ -388,7 +372,7 @@ module.exports = {
* @returns {void}
*/
"CallExpression:exit"(node) {
if (!(funcInfo && funcInfo.isConstructor)) {
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}

Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intended to report super() calls in classes that don't have extends. But such calls already cause parsing errors so that's not something a rule can catch.

},

Expand All @@ -442,7 +419,7 @@ module.exports = {
* @returns {void}
*/
ReturnStatement(node) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}

Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there was any benefit from this.

};
}
Expand Down