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

feat: Implement onUnreachableCodePathStart/End #17511

Merged
merged 34 commits into from Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ecc2dcc
feat: Implement onUnreachableCodePathStart/End
nzakas Aug 29, 2023
a402c0f
Finish up onUnreachable* work
nzakas Aug 30, 2023
40a1475
Refactor to account for out-of-order events
nzakas Sep 1, 2023
f0a093b
Update lib/rules/no-unreachable.js
nzakas Sep 5, 2023
b99db3b
Update lib/rules/no-unreachable.js
nzakas Sep 5, 2023
9e2b95c
Update tests/lib/linter/code-path-analysis/code-path-analyzer.js
nzakas Sep 5, 2023
363fd39
Incorporate feedback
nzakas Sep 5, 2023
01d1214
Clean up rules and docs
nzakas Sep 5, 2023
569175b
Update docs
nzakas Sep 5, 2023
b5b9a6a
Fix code example
nzakas Sep 5, 2023
b465903
Update docs/src/extend/code-path-analysis.md
nzakas Sep 6, 2023
0c6ffca
Update docs/src/extend/code-path-analysis.md
nzakas Sep 6, 2023
9d7c6c7
Update docs/src/extend/code-path-analysis.md
nzakas Sep 6, 2023
e044654
Update lib/rules/consistent-return.js
nzakas Sep 6, 2023
110f752
Update lib/rules/no-this-before-super.js
nzakas Sep 6, 2023
5544ba2
Fix examples
nzakas Sep 6, 2023
7ec047b
Add deprecation notices to RuleTester/FlatRuleTester
nzakas Sep 6, 2023
a945062
Update config
nzakas Sep 6, 2023
99e2ef8
chore: Upgrade config-array (#17512)
nzakas Aug 29, 2023
5972899
test: replace Karma with Webdriver.IO (#17126)
christian-bromann Aug 30, 2023
6b69804
chore: use eslint-plugin-jsdoc's flat config (#17516)
mdjermanovic Sep 1, 2023
549f4a5
docs: Update README
Sep 1, 2023
04d7ae4
docs: add typescript template (#17500)
Zamiell Sep 1, 2023
4bf7ce7
feat: add new `enforce` option to `lines-between-class-members` (#17462)
snitin315 Sep 2, 2023
fd2a909
feat: Emit deprecation warnings in RuleTester (#17527)
nzakas Sep 2, 2023
d2137ec
ci: bump actions/checkout from 3 to 4 (#17530)
dependabot[bot] Sep 4, 2023
929dd4d
docs: update `no-promise-executor-return` examples (#17529)
snitin315 Sep 6, 2023
d350fa9
Add deprecation notices to RuleTester/FlatRuleTester
nzakas Sep 6, 2023
a10206d
Merge branch 'main' into unreachable
nzakas Sep 6, 2023
b52d2a3
Fix lint warning
nzakas Sep 6, 2023
a352a8f
Update docs/src/extend/code-path-analysis.md
nzakas Sep 7, 2023
6405e23
Update docs/src/extend/code-path-analysis.md
nzakas Sep 7, 2023
0acea65
Update docs/src/extend/code-path-analysis.md
nzakas Sep 7, 2023
e444a69
Fix test
nzakas Sep 7, 2023
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
482 changes: 320 additions & 162 deletions docs/src/extend/code-path-analysis.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion eslint.config.js
Expand Up @@ -226,7 +226,6 @@ module.exports = [
files: [INTERNAL_FILES.RULE_TESTER_PATTERN],
rules: {
"n/no-restricted-require": ["error", [
...createInternalFilesPatterns(INTERNAL_FILES.RULE_TESTER_PATTERN),
resolveAbsolutePath("lib/cli-engine/index.js")
]]
}
Expand Down
56 changes: 32 additions & 24 deletions lib/linter/code-path-analysis/code-path-analyzer.js
Expand Up @@ -192,15 +192,18 @@ function forwardCurrentToHead(analyzer, node) {
headSegment = headSegments[i];

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

if (currentSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentEnd",
currentSegment,
node
);
}
const eventName = currentSegment.reachable
? "onCodePathSegmentEnd"
: "onUnreachableCodePathSegmentEnd";

debug.dump(`${eventName} ${currentSegment.id}`);

analyzer.emitter.emit(
eventName,
currentSegment,
node
);
}
}

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

if (currentSegment !== headSegment && headSegment) {
debug.dump(`onCodePathSegmentStart ${headSegment.id}`);

const eventName = headSegment.reachable
? "onCodePathSegmentStart"
: "onUnreachableCodePathSegmentStart";

debug.dump(`${eventName} ${headSegment.id}`);

CodePathSegment.markUsed(headSegment);
if (headSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentStart",
headSegment,
node
);
}
analyzer.emitter.emit(
eventName,
headSegment,
node
);
}
}

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

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

debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
if (currentSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentEnd",
currentSegment,
node
);
}
debug.dump(`${eventName} ${currentSegment.id}`);

analyzer.emitter.emit(
eventName,
currentSegment,
node
);
}

state.currentSegments = [];
Expand Down
1 change: 1 addition & 0 deletions lib/linter/code-path-analysis/code-path.js
Expand Up @@ -117,6 +117,7 @@ class CodePath {
/**
* Current code path segments.
* @type {CodePathSegment[]}
* @deprecated
*/
get currentSegments() {
return this.internal.currentSegments;
Expand Down
1 change: 1 addition & 0 deletions lib/linter/linter.js
Expand Up @@ -898,6 +898,7 @@ const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
getTokensBetween: "getTokensBetween"
};


const BASE_TRAVERSAL_CONTEXT = Object.freeze(
Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce(
(contextInfo, methodName) =>
Expand Down
29 changes: 28 additions & 1 deletion lib/rule-tester/flat-rule-tester.js
Expand Up @@ -16,7 +16,9 @@ const
equal = require("fast-deep-equal"),
Traverser = require("../shared/traverser"),
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"),
{ Linter, SourceCodeFixer, interpolate } = require("../linter");
{ Linter, SourceCodeFixer, interpolate } = require("../linter"),
CodePath = require("../linter/code-path-analysis/code-path");

const { FlatConfigArray } = require("../config/flat-config-array");
const { defaultConfig } = require("../config/default-config");

Expand Down Expand Up @@ -274,6 +276,21 @@ function getCommentsDeprecation() {
);
}

/**
* Emit a deprecation warning if rule uses CodePath#currentSegments.
* @param {string} ruleName Name of the rule.
* @returns {void}
*/
function emitCodePathCurrentSegmentsWarning(ruleName) {
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) {
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true;
process.emitWarning(
`"${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`,
"DeprecationWarning"
);
}
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -664,6 +681,7 @@ class FlatRuleTester {

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

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

try {
SourceCode.prototype.getComments = getCommentsDeprecation;
Object.defineProperty(CodePath.prototype, "currentSegments", {
get() {
emitCodePathCurrentSegmentsWarning(ruleName);
return originalCurrentSegments.get.call(this);
}
});

messages = linter.verify(code, configs, filename);
} finally {
SourceCode.prototype.getComments = getComments;
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
}


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

assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
Expand Down
27 changes: 26 additions & 1 deletion lib/rule-tester/rule-tester.js
Expand Up @@ -48,7 +48,8 @@ const
equal = require("fast-deep-equal"),
Traverser = require("../../lib/shared/traverser"),
{ getRuleOptionsSchema, validate } = require("../shared/config-validator"),
{ Linter, SourceCodeFixer, interpolate } = require("../linter");
{ Linter, SourceCodeFixer, interpolate } = require("../linter"),
CodePath = require("../linter/code-path-analysis/code-path");

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

Expand Down Expand Up @@ -375,6 +376,21 @@ function emitDeprecatedContextMethodWarning(ruleName, methodName) {
}
}

/**
* Emit a deprecation warning if rule uses CodePath#currentSegments.
* @param {string} ruleName Name of the rule.
* @returns {void}
*/
function emitCodePathCurrentSegmentsWarning(ruleName) {
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) {
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true;
process.emitWarning(
`"${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`,
"DeprecationWarning"
);
}
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -746,13 +762,22 @@ class RuleTester {

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

try {
SourceCode.prototype.getComments = getCommentsDeprecation;
Object.defineProperty(CodePath.prototype, "currentSegments", {
get() {
emitCodePathCurrentSegmentsWarning(ruleName);
return originalCurrentSegments.get.call(this);
}
});

messages = linter.verify(code, config, filename);
} finally {
SourceCode.prototype.getComments = getComments;
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
}

const fatalErrorMessage = messages.find(m => m.fatal);
Expand Down
47 changes: 36 additions & 11 deletions lib/rules/array-callback-return.js
Expand Up @@ -18,15 +18,6 @@ const astUtils = require("./utils/ast-utils");
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u;

/**
* Checks a given code path segment is reachable.
* @param {CodePathSegment} segment A segment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}

/**
* Checks a given node is a member access which has the specified name's
* property.
Expand All @@ -38,6 +29,22 @@ function isTargetMethod(node) {
return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
}

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

/**
* Returns a human-legible description of an array method
* @param {string} arrayMethodName A method name to fully qualify
Expand Down Expand Up @@ -205,7 +212,7 @@ module.exports = {
messageId = "expectedNoReturnValue";
}
} else {
if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) {
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
}
}
Expand Down Expand Up @@ -242,7 +249,8 @@ module.exports = {
methodName &&
!node.async &&
!node.generator,
node
node,
currentSegments: new Set()
};
},

Expand All @@ -251,6 +259,23 @@ module.exports = {
funcInfo = funcInfo.upper;
},

onUnreachableCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},

onUnreachableCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},

onCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},

onCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},


// Checks the return statement is valid.
ReturnStatement(node) {

Expand Down
39 changes: 32 additions & 7 deletions lib/rules/consistent-return.js
Expand Up @@ -16,12 +16,19 @@ const { upperCaseFirst } = require("../shared/string-utils");
//------------------------------------------------------------------------------

/**
* Checks whether or not a given code path segment is unreachable.
* @param {CodePathSegment} segment A CodePathSegment to check.
* @returns {boolean} `true` if the segment is unreachable.
* Checks all segments in a set and returns true if all are unreachable.
* @param {Set<CodePathSegment>} segments The segments to check.
* @returns {boolean} True if all segments are unreachable; false otherwise.
*/
function isUnreachable(segment) {
return !segment.reachable;
function areAllSegmentsUnreachable(segments) {

for (const segment of segments) {
if (segment.reachable) {
return false;
}
}

return true;
}

/**
Expand Down Expand Up @@ -88,7 +95,7 @@ module.exports = {
* When unreachable, all paths are returned or thrown.
*/
if (!funcInfo.hasReturnValue ||
funcInfo.codePath.currentSegments.every(isUnreachable) ||
areAllSegmentsUnreachable(funcInfo.currentSegments) ||
astUtils.isES5Constructor(node) ||
isClassConstructor(node)
) {
Expand Down Expand Up @@ -141,13 +148,31 @@ module.exports = {
hasReturn: false,
hasReturnValue: false,
messageId: "",
node
node,
currentSegments: new Set()
};
},
onCodePathEnd() {
funcInfo = funcInfo.upper;
},

onUnreachableCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},

onUnreachableCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},

onCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},

onCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},


// Reports a given return statement if it's inconsistent.
ReturnStatement(node) {
const argument = node.argument;
Expand Down