Skip to content

Commit da09f4e

Browse files
nzakasmdjermanovic
andauthored
feat: Implement onUnreachableCodePathStart/End (#17511)
* feat: Implement onUnreachableCodePathStart/End refs # 17457 * Finish up onUnreachable* work * Refactor to account for out-of-order events * Update lib/rules/no-unreachable.js Co-authored-by: Milos Djermanovic <[email protected]> * Update lib/rules/no-unreachable.js Co-authored-by: Milos Djermanovic <[email protected]> * Update tests/lib/linter/code-path-analysis/code-path-analyzer.js Co-authored-by: Milos Djermanovic <[email protected]> * Incorporate feedback * Clean up rules and docs * Update docs * Fix code example * Update docs/src/extend/code-path-analysis.md Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/extend/code-path-analysis.md Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/extend/code-path-analysis.md Co-authored-by: Milos Djermanovic <[email protected]> * Update lib/rules/consistent-return.js Co-authored-by: Milos Djermanovic <[email protected]> * Update lib/rules/no-this-before-super.js Co-authored-by: Milos Djermanovic <[email protected]> * Fix examples * Add deprecation notices to RuleTester/FlatRuleTester * Update config * Add deprecation notices to RuleTester/FlatRuleTester * Fix lint warning * Update docs/src/extend/code-path-analysis.md Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/extend/code-path-analysis.md Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/extend/code-path-analysis.md Co-authored-by: Milos Djermanovic <[email protected]> * Fix test --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent de86b3b commit da09f4e

20 files changed

+990
-287
lines changed

docs/src/extend/code-path-analysis.md

+320-162
Large diffs are not rendered by default.

eslint.config.js

-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ module.exports = [
226226
files: [INTERNAL_FILES.RULE_TESTER_PATTERN],
227227
rules: {
228228
"n/no-restricted-require": ["error", [
229-
...createInternalFilesPatterns(INTERNAL_FILES.RULE_TESTER_PATTERN),
230229
resolveAbsolutePath("lib/cli-engine/index.js")
231230
]]
232231
}

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

+32-24
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,18 @@ function forwardCurrentToHead(analyzer, node) {
192192
headSegment = headSegments[i];
193193

194194
if (currentSegment !== headSegment && currentSegment) {
195-
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
196195

197-
if (currentSegment.reachable) {
198-
analyzer.emitter.emit(
199-
"onCodePathSegmentEnd",
200-
currentSegment,
201-
node
202-
);
203-
}
196+
const eventName = currentSegment.reachable
197+
? "onCodePathSegmentEnd"
198+
: "onUnreachableCodePathSegmentEnd";
199+
200+
debug.dump(`${eventName} ${currentSegment.id}`);
201+
202+
analyzer.emitter.emit(
203+
eventName,
204+
currentSegment,
205+
node
206+
);
204207
}
205208
}
206209

@@ -213,16 +216,19 @@ function forwardCurrentToHead(analyzer, node) {
213216
headSegment = headSegments[i];
214217

215218
if (currentSegment !== headSegment && headSegment) {
216-
debug.dump(`onCodePathSegmentStart ${headSegment.id}`);
219+
220+
const eventName = headSegment.reachable
221+
? "onCodePathSegmentStart"
222+
: "onUnreachableCodePathSegmentStart";
223+
224+
debug.dump(`${eventName} ${headSegment.id}`);
217225

218226
CodePathSegment.markUsed(headSegment);
219-
if (headSegment.reachable) {
220-
analyzer.emitter.emit(
221-
"onCodePathSegmentStart",
222-
headSegment,
223-
node
224-
);
225-
}
227+
analyzer.emitter.emit(
228+
eventName,
229+
headSegment,
230+
node
231+
);
226232
}
227233
}
228234

@@ -241,15 +247,17 @@ function leaveFromCurrentSegment(analyzer, node) {
241247

242248
for (let i = 0; i < currentSegments.length; ++i) {
243249
const currentSegment = currentSegments[i];
250+
const eventName = currentSegment.reachable
251+
? "onCodePathSegmentEnd"
252+
: "onUnreachableCodePathSegmentEnd";
244253

245-
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
246-
if (currentSegment.reachable) {
247-
analyzer.emitter.emit(
248-
"onCodePathSegmentEnd",
249-
currentSegment,
250-
node
251-
);
252-
}
254+
debug.dump(`${eventName} ${currentSegment.id}`);
255+
256+
analyzer.emitter.emit(
257+
eventName,
258+
currentSegment,
259+
node
260+
);
253261
}
254262

255263
state.currentSegments = [];

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

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class CodePath {
117117
/**
118118
* Current code path segments.
119119
* @type {CodePathSegment[]}
120+
* @deprecated
120121
*/
121122
get currentSegments() {
122123
return this.internal.currentSegments;

lib/linter/linter.js

+1
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,7 @@ const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
898898
getTokensBetween: "getTokensBetween"
899899
};
900900

901+
901902
const BASE_TRAVERSAL_CONTEXT = Object.freeze(
902903
Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce(
903904
(contextInfo, methodName) =>

lib/rule-tester/flat-rule-tester.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ const
1616
equal = require("fast-deep-equal"),
1717
Traverser = require("../shared/traverser"),
1818
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"),
19-
{ Linter, SourceCodeFixer, interpolate } = require("../linter");
19+
{ Linter, SourceCodeFixer, interpolate } = require("../linter"),
20+
CodePath = require("../linter/code-path-analysis/code-path");
21+
2022
const { FlatConfigArray } = require("../config/flat-config-array");
2123
const { defaultConfig } = require("../config/default-config");
2224

@@ -274,6 +276,21 @@ function getCommentsDeprecation() {
274276
);
275277
}
276278

279+
/**
280+
* Emit a deprecation warning if rule uses CodePath#currentSegments.
281+
* @param {string} ruleName Name of the rule.
282+
* @returns {void}
283+
*/
284+
function emitCodePathCurrentSegmentsWarning(ruleName) {
285+
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) {
286+
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true;
287+
process.emitWarning(
288+
`"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`,
289+
"DeprecationWarning"
290+
);
291+
}
292+
}
293+
277294
//------------------------------------------------------------------------------
278295
// Public Interface
279296
//------------------------------------------------------------------------------
@@ -664,6 +681,7 @@ class FlatRuleTester {
664681

665682
// Verify the code.
666683
const { getComments } = SourceCode.prototype;
684+
const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments");
667685
let messages;
668686

669687
// check for validation errors
@@ -677,11 +695,20 @@ class FlatRuleTester {
677695

678696
try {
679697
SourceCode.prototype.getComments = getCommentsDeprecation;
698+
Object.defineProperty(CodePath.prototype, "currentSegments", {
699+
get() {
700+
emitCodePathCurrentSegmentsWarning(ruleName);
701+
return originalCurrentSegments.get.call(this);
702+
}
703+
});
704+
680705
messages = linter.verify(code, configs, filename);
681706
} finally {
682707
SourceCode.prototype.getComments = getComments;
708+
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
683709
}
684710

711+
685712
const fatalErrorMessage = messages.find(m => m.fatal);
686713

687714
assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);

lib/rule-tester/rule-tester.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ const
4848
equal = require("fast-deep-equal"),
4949
Traverser = require("../../lib/shared/traverser"),
5050
{ getRuleOptionsSchema, validate } = require("../shared/config-validator"),
51-
{ Linter, SourceCodeFixer, interpolate } = require("../linter");
51+
{ Linter, SourceCodeFixer, interpolate } = require("../linter"),
52+
CodePath = require("../linter/code-path-analysis/code-path");
5253

5354
const ajv = require("../shared/ajv")({ strictDefaults: true });
5455

@@ -375,6 +376,21 @@ function emitDeprecatedContextMethodWarning(ruleName, methodName) {
375376
}
376377
}
377378

379+
/**
380+
* Emit a deprecation warning if rule uses CodePath#currentSegments.
381+
* @param {string} ruleName Name of the rule.
382+
* @returns {void}
383+
*/
384+
function emitCodePathCurrentSegmentsWarning(ruleName) {
385+
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) {
386+
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true;
387+
process.emitWarning(
388+
`"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`,
389+
"DeprecationWarning"
390+
);
391+
}
392+
}
393+
378394
//------------------------------------------------------------------------------
379395
// Public Interface
380396
//------------------------------------------------------------------------------
@@ -746,13 +762,22 @@ class RuleTester {
746762

747763
// Verify the code.
748764
const { getComments } = SourceCode.prototype;
765+
const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments");
749766
let messages;
750767

751768
try {
752769
SourceCode.prototype.getComments = getCommentsDeprecation;
770+
Object.defineProperty(CodePath.prototype, "currentSegments", {
771+
get() {
772+
emitCodePathCurrentSegmentsWarning(ruleName);
773+
return originalCurrentSegments.get.call(this);
774+
}
775+
});
776+
753777
messages = linter.verify(code, config, filename);
754778
} finally {
755779
SourceCode.prototype.getComments = getComments;
780+
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
756781
}
757782

758783
const fatalErrorMessage = messages.find(m => m.fatal);

lib/rules/array-callback-return.js

+36-11
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,6 @@ const astUtils = require("./utils/ast-utils");
1818
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
1919
const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u;
2020

21-
/**
22-
* Checks a given code path segment is reachable.
23-
* @param {CodePathSegment} segment A segment to check.
24-
* @returns {boolean} `true` if the segment is reachable.
25-
*/
26-
function isReachable(segment) {
27-
return segment.reachable;
28-
}
29-
3021
/**
3122
* Checks a given node is a member access which has the specified name's
3223
* property.
@@ -38,6 +29,22 @@ function isTargetMethod(node) {
3829
return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
3930
}
4031

32+
/**
33+
* Checks all segments in a set and returns true if any are reachable.
34+
* @param {Set<CodePathSegment>} segments The segments to check.
35+
* @returns {boolean} True if any segment is reachable; false otherwise.
36+
*/
37+
function isAnySegmentReachable(segments) {
38+
39+
for (const segment of segments) {
40+
if (segment.reachable) {
41+
return true;
42+
}
43+
}
44+
45+
return false;
46+
}
47+
4148
/**
4249
* Returns a human-legible description of an array method
4350
* @param {string} arrayMethodName A method name to fully qualify
@@ -205,7 +212,7 @@ module.exports = {
205212
messageId = "expectedNoReturnValue";
206213
}
207214
} else {
208-
if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) {
215+
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
209216
messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
210217
}
211218
}
@@ -242,7 +249,8 @@ module.exports = {
242249
methodName &&
243250
!node.async &&
244251
!node.generator,
245-
node
252+
node,
253+
currentSegments: new Set()
246254
};
247255
},
248256

@@ -251,6 +259,23 @@ module.exports = {
251259
funcInfo = funcInfo.upper;
252260
},
253261

262+
onUnreachableCodePathSegmentStart(segment) {
263+
funcInfo.currentSegments.add(segment);
264+
},
265+
266+
onUnreachableCodePathSegmentEnd(segment) {
267+
funcInfo.currentSegments.delete(segment);
268+
},
269+
270+
onCodePathSegmentStart(segment) {
271+
funcInfo.currentSegments.add(segment);
272+
},
273+
274+
onCodePathSegmentEnd(segment) {
275+
funcInfo.currentSegments.delete(segment);
276+
},
277+
278+
254279
// Checks the return statement is valid.
255280
ReturnStatement(node) {
256281

lib/rules/consistent-return.js

+32-7
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ const { upperCaseFirst } = require("../shared/string-utils");
1616
//------------------------------------------------------------------------------
1717

1818
/**
19-
* Checks whether or not a given code path segment is unreachable.
20-
* @param {CodePathSegment} segment A CodePathSegment to check.
21-
* @returns {boolean} `true` if the segment is unreachable.
19+
* Checks all segments in a set and returns true if all are unreachable.
20+
* @param {Set<CodePathSegment>} segments The segments to check.
21+
* @returns {boolean} True if all segments are unreachable; false otherwise.
2222
*/
23-
function isUnreachable(segment) {
24-
return !segment.reachable;
23+
function areAllSegmentsUnreachable(segments) {
24+
25+
for (const segment of segments) {
26+
if (segment.reachable) {
27+
return false;
28+
}
29+
}
30+
31+
return true;
2532
}
2633

2734
/**
@@ -88,7 +95,7 @@ module.exports = {
8895
* When unreachable, all paths are returned or thrown.
8996
*/
9097
if (!funcInfo.hasReturnValue ||
91-
funcInfo.codePath.currentSegments.every(isUnreachable) ||
98+
areAllSegmentsUnreachable(funcInfo.currentSegments) ||
9299
astUtils.isES5Constructor(node) ||
93100
isClassConstructor(node)
94101
) {
@@ -141,13 +148,31 @@ module.exports = {
141148
hasReturn: false,
142149
hasReturnValue: false,
143150
messageId: "",
144-
node
151+
node,
152+
currentSegments: new Set()
145153
};
146154
},
147155
onCodePathEnd() {
148156
funcInfo = funcInfo.upper;
149157
},
150158

159+
onUnreachableCodePathSegmentStart(segment) {
160+
funcInfo.currentSegments.add(segment);
161+
},
162+
163+
onUnreachableCodePathSegmentEnd(segment) {
164+
funcInfo.currentSegments.delete(segment);
165+
},
166+
167+
onCodePathSegmentStart(segment) {
168+
funcInfo.currentSegments.add(segment);
169+
},
170+
171+
onCodePathSegmentEnd(segment) {
172+
funcInfo.currentSegments.delete(segment);
173+
},
174+
175+
151176
// Reports a given return statement if it's inconsistent.
152177
ReturnStatement(node) {
153178
const argument = node.argument;

0 commit comments

Comments
 (0)