Skip to content

Commit 26093c7

Browse files
authored
feat: fix false negatives in no-this-before-super (#17762)
* fix: false negatives in `no-this-before-super` * chore: move comment * fix: traverseSegments controller.skip * chore: format * chore: add a little early judgment * fix: remove skipped.has(segment) * fix: remove unnecessary code * fix: wrong traverse * fix: rename variable
1 parent 57089cb commit 26093c7

File tree

4 files changed

+192
-16
lines changed

4 files changed

+192
-16
lines changed

lib/linter/code-path-analysis/code-path.js

+28-12
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ class CodePath {
177177
// tracks the traversal steps
178178
const stack = [[startSegment, 0]];
179179

180-
// tracks the last skipped segment during traversal
181-
let skippedSegment = null;
180+
// segments that have been skipped during traversal
181+
const skipped = new Set();
182182

183183
// indicates if we exited early from the traversal
184184
let broken = false;
@@ -193,11 +193,7 @@ class CodePath {
193193
* @returns {void}
194194
*/
195195
skip() {
196-
if (stack.length <= 1) {
197-
broken = true;
198-
} else {
199-
skippedSegment = stack.at(-2)[0];
200-
}
196+
skipped.add(segment);
201197
},
202198

203199
/**
@@ -222,6 +218,18 @@ class CodePath {
222218
);
223219
}
224220

221+
/**
222+
* Checks if a given previous segment has been skipped.
223+
* @param {CodePathSegment} prevSegment A previous segment to check.
224+
* @returns {boolean} `true` if the segment has been skipped.
225+
*/
226+
function isSkipped(prevSegment) {
227+
return (
228+
skipped.has(prevSegment) ||
229+
segment.isLoopedPrevSegment(prevSegment)
230+
);
231+
}
232+
225233
// the traversal
226234
while (stack.length > 0) {
227235

@@ -258,17 +266,21 @@ class CodePath {
258266
continue;
259267
}
260268

261-
// Reset the skipping flag if all branches have been skipped.
262-
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
263-
skippedSegment = null;
264-
}
265269
visited.add(segment);
266270

271+
272+
// Skips the segment if all previous segments have been skipped.
273+
const shouldSkip = (
274+
skipped.size > 0 &&
275+
segment.prevSegments.length > 0 &&
276+
segment.prevSegments.every(isSkipped)
277+
);
278+
267279
/*
268280
* If the most recent segment hasn't been skipped, then we call
269281
* the callback, passing in the segment and the controller.
270282
*/
271-
if (!skippedSegment) {
283+
if (!shouldSkip) {
272284
resolvedCallback.call(this, segment, controller);
273285

274286
// exit if we're at the last segment
@@ -284,6 +296,10 @@ class CodePath {
284296
if (broken) {
285297
break;
286298
}
299+
} else {
300+
301+
// If the most recent segment has been skipped, then mark it as skipped.
302+
skipped.add(segment);
287303
}
288304
}
289305

lib/rules/no-this-before-super.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,26 @@ module.exports = {
197197
return;
198198
}
199199

200+
/**
201+
* A collection of nodes to avoid duplicate reports.
202+
* @type {Set<ASTNode>}
203+
*/
204+
const reported = new Set();
205+
200206
codePath.traverseSegments((segment, controller) => {
201207
const info = segInfoMap[segment.id];
208+
const invalidNodes = info.invalidNodes
209+
.filter(
210+
211+
/*
212+
* Avoid duplicate reports.
213+
* When there is a `finally`, invalidNodes may contain already reported node.
214+
*/
215+
node => !reported.has(node)
216+
);
202217

203-
for (let i = 0; i < info.invalidNodes.length; ++i) {
204-
const invalidNode = info.invalidNodes[i];
218+
for (const invalidNode of invalidNodes) {
219+
reported.add(invalidNode);
205220

206221
context.report({
207222
messageId: "noBeforeSuper",
@@ -273,14 +288,12 @@ module.exports = {
273288
const info = segInfoMap[segment.id];
274289

275290
if (info.superCalled) {
276-
info.invalidNodes = [];
277291
controller.skip();
278292
} else if (
279293
segment.prevSegments.length > 0 &&
280294
segment.prevSegments.every(isCalled)
281295
) {
282296
info.superCalled = true;
283-
info.invalidNodes = [];
284297
}
285298
}
286299
);

tests/lib/linter/code-path-analysis/code-path.js

+52
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,58 @@ describe("CodePathAnalyzer", () => {
356356
*/
357357
});
358358

359+
it("should not skip the next branch when 'controller.skip()' was called.", () => {
360+
const codePath = parseCodePaths("if (a) { if (b) { foo(); } bar(); } out1();")[0];
361+
const order = getOrderOfTraversing(codePath, null, (segment, controller) => {
362+
if (segment.id === "s1_4") {
363+
controller.skip(); // Since s1_5 is connected from s1_1, we expect it not to be skipped.
364+
}
365+
});
366+
367+
assert.deepStrictEqual(order, ["s1_1", "s1_2", "s1_3", "s1_4", "s1_5"]);
368+
369+
/*
370+
digraph {
371+
node[shape=box,style="rounded,filled",fillcolor=white];
372+
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
373+
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
374+
s1_1[label="Program:enter\nIfStatement:enter\nIdentifier (a)"];
375+
s1_2[label="BlockStatement:enter\nIfStatement:enter\nIdentifier (b)"];
376+
s1_3[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (foo)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"];
377+
s1_4[label="IfStatement:exit\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (bar)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"];
378+
s1_5[label="IfStatement:exit\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (out1)\nCallExpression:exit\nExpressionStatement:exit\nProgram:exit"];
379+
initial->s1_1->s1_2->s1_3->s1_4->s1_5;
380+
s1_1->s1_5;
381+
s1_2->s1_4;
382+
s1_5->final;
383+
}
384+
*/
385+
});
386+
387+
it("should skip the next branch when 'controller.skip()' was called at top segment.", () => {
388+
const codePath = parseCodePaths("a; while (b) { c; }")[0];
389+
390+
const order = getOrderOfTraversing(codePath, null, (segment, controller) => {
391+
if (segment.id === "s1_1") {
392+
controller.skip();
393+
}
394+
});
395+
396+
assert.deepStrictEqual(order, ["s1_1"]);
397+
398+
/*
399+
digraph {
400+
node[shape=box,style="rounded,filled",fillcolor=white];
401+
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
402+
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
403+
s1_1[label="Program:enter\nExpressionStatement:enter\nIdentifier (a)\nExpressionStatement:exit\nWhileStatement:enter"];
404+
s1_2[label="Identifier (b)"];
405+
s1_3[label="BlockStatement:enter\nExpressionStatement:enter\nIdentifier (c)\nExpressionStatement:exit\nBlockStatement:exit"];
406+
s1_4[label="WhileStatement:exit\nProgram:exit"];
407+
initial->s1_1->s1_2->s1_3->s1_2->s1_4->final;
408+
}
409+
*/
410+
});
359411
/* eslint-enable internal-rules/multiline-comment-style -- Commenting out */
360412
});
361413
});

tests/lib/rules/no-this-before-super.js

+95
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ ruleTester.run("no-this-before-super", rule, {
6060
"class A extends B { constructor() { if (a) { super(); this.a(); } else { super(); this.b(); } } }",
6161
"class A extends B { constructor() { if (a) super(); else super(); this.a(); } }",
6262
"class A extends B { constructor() { try { super(); } finally {} this.a(); } }",
63+
`class A extends B {
64+
constructor() {
65+
while (foo) {
66+
super();
67+
this.a();
68+
}
69+
}
70+
}`,
71+
`class A extends B {
72+
constructor() {
73+
while (foo) {
74+
if (init) {
75+
super();
76+
this.a();
77+
}
78+
}
79+
}
80+
}`,
6381

6482
// https://github.com/eslint/eslint/issues/5261
6583
"class A extends B { constructor(a) { super(); for (const b of a) { this.a(); } } }",
@@ -186,6 +204,83 @@ ruleTester.run("no-this-before-super", rule, {
186204
code: "class A extends B { constructor() { foo ??= super().a; this.c(); } }",
187205
languageOptions: { ecmaVersion: 2021 },
188206
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
207+
},
208+
{
209+
code: `
210+
class A extends B {
211+
constructor() {
212+
if (foo) {
213+
if (bar) { }
214+
super();
215+
}
216+
this.a();
217+
}
218+
}`,
219+
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
220+
},
221+
{
222+
code: `
223+
class A extends B {
224+
constructor() {
225+
if (foo) {
226+
} else {
227+
super();
228+
}
229+
this.a();
230+
}
231+
}`,
232+
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
233+
},
234+
{
235+
code: `
236+
class A extends B {
237+
constructor() {
238+
try {
239+
call();
240+
} finally {
241+
this.a();
242+
}
243+
}
244+
}`,
245+
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
246+
},
247+
{
248+
code: `
249+
class A extends B {
250+
constructor() {
251+
while (foo) {
252+
super();
253+
}
254+
this.a();
255+
}
256+
}`,
257+
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
258+
},
259+
{
260+
code: `
261+
class A extends B {
262+
constructor() {
263+
while (foo) {
264+
this.a();
265+
super();
266+
}
267+
}
268+
}`,
269+
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
270+
},
271+
{
272+
code: `
273+
class A extends B {
274+
constructor() {
275+
while (foo) {
276+
if (init) {
277+
this.a();
278+
super();
279+
}
280+
}
281+
}
282+
}`,
283+
errors: [{ messageId: "noBeforeSuper", data: { kind: "this" }, type: "ThisExpression" }]
189284
}
190285
]
191286
});

0 commit comments

Comments
 (0)