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 3 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
98 changes: 72 additions & 26 deletions docs/src/extend/code-path-analysis.md
Expand Up @@ -69,7 +69,7 @@ module.exports = function(context) {
* @param {ASTNode} node - The current node.
* @returns {void}
*/
"onCodePathStart": function(codePath, node) {
onCodePathStart(codePath, node) {
// do something with codePath
},

Expand All @@ -81,12 +81,12 @@ module.exports = function(context) {
* @param {ASTNode} node - The current node.
* @returns {void}
*/
"onCodePathEnd": function(codePath, node) {
onCodePathEnd(codePath, node) {
// do something with codePath
},

/**
* This is called when a code path segment was created.
* This is called when a reachable code path segment was created.
* It meant the code path is forked or merged.
* In this time, the segment has the previous segments and has been
* judged reachable or not.
Expand All @@ -95,19 +95,45 @@ module.exports = function(context) {
* @param {ASTNode} node - The current node.
* @returns {void}
*/
"onCodePathSegmentStart": function(segment, node) {
onCodePathSegmentStart(segment, node) {
// do something with segment
},

/**
* This is called when a code path segment was left.
* This is called when a reachable code path segment was left.
* In this time, the segment does not have the next segments yet.
*
* @param {CodePathSegment} segment - The left code path segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
"onCodePathSegmentEnd": function(segment, node) {
onCodePathSegmentEnd(segment, node) {
// do something with segment
},

/**
* This is called when an unreachable code path segment was created.
* It meant the code path is forked or merged.
* In this time, the segment has the previous segments and has been
* judged reachable or not.
*
* @param {CodePathSegment} segment - The new code path segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onUnreachableCodePathSegmentStart(segment, node) {
// do something with segment
},

/**
* This is called when an unreachable code path segment was left.
* In this time, the segment does not have the next segments yet.
*
* @param {CodePathSegment} segment - The left code path segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onUnreachableCodePathSegmentEnd(segment, node) {
// do something with segment
},

Expand All @@ -122,7 +148,7 @@ module.exports = function(context) {
* @param {ASTNode} node - The current node.
* @returns {void}
*/
"onCodePathSegmentLoop": function(fromSegment, toSegment, node) {
"onCodePathSegmentLoop(fromSegment, toSegment, node) {
// do something with segment
}
};
Expand Down Expand Up @@ -272,23 +298,28 @@ function isCbCalled(info) {
}

module.exports = function(context) {
var funcInfoStack = [];
var segmentInfoMap = Object.create(null);
let funcInfo;
const funcInfoStack = [];
const segmentInfoMap = Object.create(null);

return {
// Checks `cb`.
"onCodePathStart": function(codePath, node) {
funcInfoStack.push({
onCodePathStart(codePath, node) {
funcInfo = {
codePath: codePath,
hasCb: hasCb(node, context)
});
hasCb: hasCb(node, context),
currentSegments: []
};

funcInfoStack.push(funcInfo);
},
"onCodePathEnd": function(codePath, node) {
funcInfoStack.pop();

onCodePathEnd(codePath, node) {
funcInfo = funcInfoStack.pop();
nzakas marked this conversation as resolved.
Show resolved Hide resolved

// Checks `cb` was called in every paths.
var cbCalled = codePath.finalSegments.every(function(segment) {
var info = segmentInfoMap[segment.id];
const cbCalled = codePath.finalSegments.every(function(segment) {
const info = segmentInfoMap[segment.id];
return info.cbCalled;
});

Expand All @@ -300,17 +331,18 @@ module.exports = function(context) {
}
},

// Manages state of code paths.
"onCodePathSegmentStart": function(segment) {
var funcInfo = funcInfoStack[funcInfoStack.length - 1];
// Manages state of code paths and tracks traversed segments
onCodePathSegmentStart(segment) {

funcInfo.currentSegments.push(segment);

// Ignores if `cb` doesn't exist.
if (!funcInfo.hasCb) {
return;
}
nzakas marked this conversation as resolved.
Show resolved Hide resolved

// Initialize state of this path.
var info = segmentInfoMap[segment.id] = {
const info = segmentInfoMap[segment.id] = {
cbCalled: false
};

Expand All @@ -321,20 +353,34 @@ module.exports = function(context) {
}
nzakas marked this conversation as resolved.
Show resolved Hide resolved
},

// Tracks unreachable segment traversal
onUnreachableCodePathSegmentStart(segment) {
funcInfo.currentSegments.push(segment);
}

// Tracks reachable segment traversal
onCodePathSegmentEnd() {
funcInfo.currentSegments.pop();
}

// Tracks unreachable segment traversal
onUnreachableCodePathSegmentEnd() {
funcInfo.currentSegments.pop();
}

// Checks reachable or not.
"CallExpression": function(node) {
var funcInfo = funcInfoStack[funcInfoStack.length - 1];
CallExpression(node) {

// Ignores if `cb` doesn't exist.
if (!funcInfo.hasCb) {
return;
}
nzakas marked this conversation as resolved.
Show resolved Hide resolved

// Sets marks that `cb` was called.
var callee = node.callee;
const callee = node.callee;
if (callee.type === "Identifier" && callee.name === "cb") {
funcInfo.codePath.currentSegments.forEach(function(segment) {
var info = segmentInfoMap[segment.id];
funcInfo.currentSegments.forEach(segment => {
const info = segmentInfoMap[segment.id];
info.cbCalled = true;
});
}
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/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
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