diff --git a/docs/src/extend/code-path-analysis.md b/docs/src/extend/code-path-analysis.md index 7344f8647ad..87911957490 100644 --- a/docs/src/extend/code-path-analysis.md +++ b/docs/src/extend/code-path-analysis.md @@ -37,7 +37,7 @@ This has references of both the initial segment and the final segments of a code * `finalSegments` (`CodePathSegment[]`) - The final segments which includes both returned and thrown. * `returnedSegments` (`CodePathSegment[]`) - The final segments which includes only returned. * `thrownSegments` (`CodePathSegment[]`) - The final segments which includes only thrown. -* `currentSegments` (`CodePathSegment[]`) - Segments of the current position. +* `currentSegments` (`CodePathSegment[]`) - **Deprecated.** Segments of the current traversal position. * `upper` (`CodePath|null`) - The code path of the upper function/global scope. * `childCodePaths` (`CodePath[]`) - Code paths of functions this code path contains. @@ -56,77 +56,110 @@ Difference from doubly linked list is what there are forking and merging (the ne ## Events -There are five events related to code paths, and you can define event handlers in rules. +There are seven events related to code paths, and you can define event handlers by adding them alongside node visitors in the object exported from the `create()` method of your rule. ```js -module.exports = function(context) { - return { - /** - * This is called at the start of analyzing a code path. - * In this time, the code path object has only the initial segment. - * - * @param {CodePath} codePath - The new code path. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathStart": function(codePath, node) { - // do something with codePath - }, - - /** - * This is called at the end of analyzing a code path. - * In this time, the code path object is complete. - * - * @param {CodePath} codePath - The completed code path. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathEnd": function(codePath, node) { - // do something with codePath - }, - - /** - * This is called when a 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} - */ - "onCodePathSegmentStart": function(segment, node) { - // do something with segment - }, - - /** - * This is called when a 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) { - // do something with segment - }, - - /** - * This is called when a code path segment was looped. - * Usually segments have each previous segments when created, - * but when looped, a segment is added as a new previous segment into a - * existing segment. - * - * @param {CodePathSegment} fromSegment - A code path segment of source. - * @param {CodePathSegment} toSegment - A code path segment of destination. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathSegmentLoop": function(fromSegment, toSegment, node) { - // do something with segment - } - }; -}; +module.exports = { + meta: { + // ... + }, + create(context) { + + return { + /** + * This is called at the start of analyzing a code path. + * In this time, the code path object has only the initial segment. + * + * @param {CodePath} codePath - The new code path. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathStart(codePath, node) { + // do something with codePath + }, + + /** + * This is called at the end of analyzing a code path. + * In this time, the code path object is complete. + * + * @param {CodePath} codePath - The completed code path. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathEnd(codePath, node) { + // do something with codePath + }, + + /** + * 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. + * + * @param {CodePathSegment} segment - The new code path segment. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathSegmentStart(segment, node) { + // do something with segment + }, + + /** + * 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(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 + }, + + /** + * This is called when a code path segment was looped. + * Usually segments have each previous segments when created, + * but when looped, a segment is added as a new previous segment into a + * existing segment. + * + * @param {CodePathSegment} fromSegment - A code path segment of source. + * @param {CodePathSegment} toSegment - A code path segment of destination. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathSegmentLoop(fromSegment, toSegment, node) { + // do something with segment + } + }; + + } +} ``` ### About `onCodePathSegmentLoop` @@ -212,35 +245,134 @@ bar(); ## Usage Examples -### To check whether or not this is reachable +### Track current segment position + +To track the current code path segment position, you can define a rule like this: ```js -function isReachable(segment) { - return segment.reachable; -} +module.exports = { + meta: { + // ... + }, + create(context) { + + // tracks the code path we are currently in + let currentCodePath; + + // tracks the segments we've traversed in the current code path + let currentSegments; + + // tracks all current segments for all open paths + const allCurrentSegments = []; + + return { + + onCodePathStart(codePath) { + currentCodePath = codePath; + allCurrentSegments.push(currentSegments); + currentSegments = new Set(); + }, + + onCodePathEnd(codePath) { + currentCodePath = codePath.upper; + currentSegments = allCurrentSegments.pop(); + }, + + onCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + currentSegments.delete(segment); + }, -module.exports = function(context) { - var codePathStack = []; - - return { - // Stores CodePath objects. - "onCodePathStart": function(codePath) { - codePathStack.push(codePath); - }, - "onCodePathEnd": function(codePath) { - codePathStack.pop(); - }, - - // Checks reachable or not. - "ExpressionStatement": function(node) { - var codePath = codePathStack[codePathStack.length - 1]; - - // Checks the current code path segments. - if (!codePath.currentSegments.some(isReachable)) { - context.report({message: "Unreachable!", node: node}); + onUnreachableCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentSegments.delete(segment); } + }; + + } +}; +``` + +In this example, the `currentCodePath` variable is used to access the code path that is currently being traversed and the `currentSegments` variable tracks the segments in that code path that have been traversed to that point. Note that `currentSegments` both starts and ends as an empty set, constantly being updated as the traversal progresses. + +Tracking the current segment position is helpful for analyzing the code path that led to a particular node, as in the next example. + +### Find an unreachable node + +To find an unreachable node, track the current segment position and then use a node visitor to check if any of the segments are reachable. For example, the following looks for any `ExpressionStatement` that is unreachable. + +```js +function areAnySegmentsReachable(segments) { + for (const segment of segments) { + if (segment.reachable) { + return true; } - }; + } + + return false; +} + +module.exports = { + meta: { + // ... + }, + create(context) { + + // tracks the code path we are currently in + let currentCodePath; + + // tracks the segments we've traversed in the current code path + let currentSegments; + + // tracks all current segments for all open paths + const allCurrentSegments = []; + + return { + + onCodePathStart(codePath) { + currentCodePath = codePath; + allCurrentSegments.push(currentSegments); + currentSegments = new Set(); + }, + + onCodePathEnd(codePath) { + currentCodePath = codePath.upper; + currentSegments = allCurrentSegments.pop(); + }, + + onCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + currentSegments.delete(segment); + }, + + onUnreachableCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentSegments.delete(segment); + }, + + ExpressionStatement(node) { + + // check all the code path segments that led to this node + if (!areAnySegmentsReachable(currentSegments)) { + context.report({ message: "Unreachable!", node }); + } + } + + }; + + } }; ``` @@ -249,9 +381,9 @@ See Also: [no-fallthrough](https://github.com/eslint/eslint/blob/HEAD/lib/rules/no-fallthrough.js), [consistent-return](https://github.com/eslint/eslint/blob/HEAD/lib/rules/consistent-return.js) -### To check state of a code path +### Check if a function is called in every path -This example is checking whether or not the parameter `cb` is called in every path. +This example checks whether or not the parameter `cb` is called in every path. Instances of `CodePath` and `CodePathSegment` are shared to every rule. So a rule must not modify those instances. Please use a map of information instead. @@ -271,75 +403,101 @@ function isCbCalled(info) { return info.cbCalled; } -module.exports = function(context) { - var funcInfoStack = []; - var segmentInfoMap = Object.create(null); - - return { - // Checks `cb`. - "onCodePathStart": function(codePath, node) { - funcInfoStack.push({ - codePath: codePath, - hasCb: hasCb(node, context) - }); - }, - "onCodePathEnd": function(codePath, node) { - funcInfoStack.pop(); - - // Checks `cb` was called in every paths. - var cbCalled = codePath.finalSegments.every(function(segment) { - var info = segmentInfoMap[segment.id]; - return info.cbCalled; - }); - - if (!cbCalled) { - context.report({ - message: "`cb` should be called in every path.", - node: node +module.exports = { + meta: { + // ... + }, + create(context) { + + let funcInfo; + const funcInfoStack = []; + const segmentInfoMap = Object.create(null); + + return { + // Checks `cb`. + onCodePathStart(codePath, node) { + funcInfoStack.push(funcInfo); + + funcInfo = { + codePath: codePath, + hasCb: hasCb(node, context), + currentSegments: new Set() + }; + }, + + onCodePathEnd(codePath, node) { + funcInfo = funcInfoStack.pop(); + + // Checks `cb` was called in every paths. + const cbCalled = codePath.finalSegments.every(function(segment) { + const info = segmentInfoMap[segment.id]; + return info.cbCalled; }); - } - }, - - // Manages state of code paths. - "onCodePathSegmentStart": function(segment) { - var funcInfo = funcInfoStack[funcInfoStack.length - 1]; - // Ignores if `cb` doesn't exist. - if (!funcInfo.hasCb) { - return; + if (!cbCalled) { + context.report({ + message: "`cb` should be called in every path.", + node: node + }); + } + }, + + // Manages state of code paths and tracks traversed segments + onCodePathSegmentStart(segment) { + + funcInfo.currentSegments.add(segment); + + // Ignores if `cb` doesn't exist. + if (!funcInfo.hasCb) { + return; + } + + // Initialize state of this path. + const info = segmentInfoMap[segment.id] = { + cbCalled: false + }; + + // If there are the previous paths, merges state. + // Checks `cb` was called in every previous path. + if (segment.prevSegments.length > 0) { + info.cbCalled = segment.prevSegments.every(isCbCalled); + } + }, + + // Tracks unreachable segment traversal + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + // Tracks reachable segment traversal + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + // Tracks unreachable segment traversal + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + // Checks reachable or not. + CallExpression(node) { + + // Ignores if `cb` doesn't exist. + if (!funcInfo.hasCb) { + return; + } + + // Sets marks that `cb` was called. + const callee = node.callee; + if (callee.type === "Identifier" && callee.name === "cb") { + funcInfo.currentSegments.forEach(segment => { + const info = segmentInfoMap[segment.id]; + info.cbCalled = true; + }); + } } - - // Initialize state of this path. - var info = segmentInfoMap[segment.id] = { - cbCalled: false - }; - - // If there are the previous paths, merges state. - // Checks `cb` was called in every previous path. - if (segment.prevSegments.length > 0) { - info.cbCalled = segment.prevSegments.every(isCbCalled); - } - }, - - // Checks reachable or not. - "CallExpression": function(node) { - var funcInfo = funcInfoStack[funcInfoStack.length - 1]; - - // Ignores if `cb` doesn't exist. - if (!funcInfo.hasCb) { - return; - } - - // Sets marks that `cb` was called. - var callee = node.callee; - if (callee.type === "Identifier" && callee.name === "cb") { - funcInfo.codePath.currentSegments.forEach(function(segment) { - var info = segmentInfoMap[segment.id]; - info.cbCalled = true; - }); - } - } - }; + }; + } }; ``` diff --git a/eslint.config.js b/eslint.config.js index 214be713d4c..40ebe8c08f7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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") ]] } diff --git a/lib/linter/code-path-analysis/code-path-analyzer.js b/lib/linter/code-path-analysis/code-path-analyzer.js index 2dcc2734884..b60e55c16de 100644 --- a/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/lib/linter/code-path-analysis/code-path-analyzer.js @@ -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 + ); } } @@ -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 + ); } } @@ -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 = []; diff --git a/lib/linter/code-path-analysis/code-path.js b/lib/linter/code-path-analysis/code-path.js index a028ca69481..f6a88a00af9 100644 --- a/lib/linter/code-path-analysis/code-path.js +++ b/lib/linter/code-path-analysis/code-path.js @@ -117,6 +117,7 @@ class CodePath { /** * Current code path segments. * @type {CodePathSegment[]} + * @deprecated */ get currentSegments() { return this.internal.currentSegments; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 233cbed5b5c..48b2bdbe5c3 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -898,6 +898,7 @@ const DEPRECATED_SOURCECODE_PASSTHROUGHS = { getTokensBetween: "getTokensBetween" }; + const BASE_TRAVERSAL_CONTEXT = Object.freeze( Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce( (contextInfo, methodName) => diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 5a93be1cce8..d5f5981e67e 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -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"); @@ -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 //------------------------------------------------------------------------------ @@ -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 @@ -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}`); diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index c9c18664528..82d79790a31 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -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 }); @@ -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 //------------------------------------------------------------------------------ @@ -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); diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 05cd4ede966..24a33d16c99 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -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. @@ -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} 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 @@ -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"; } } @@ -242,7 +249,8 @@ module.exports = { methodName && !node.async && !node.generator, - node + node, + currentSegments: new Set() }; }, @@ -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) { diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index e2d3f078270..304e924b14a 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -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} 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; } /** @@ -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) ) { @@ -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; diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 5f405881252..330be80f386 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -10,12 +10,19 @@ //------------------------------------------------------------------------------ /** - * Checks whether a given code path segment is reachable or not. - * @param {CodePathSegment} segment A code path segment to check. - * @returns {boolean} `true` if the segment is reachable. + * Checks all segments in a set and returns true if any are reachable. + * @param {Set} segments The segments to check. + * @returns {boolean} True if any segment is reachable; false otherwise. */ -function isReachable(segment) { - return segment.reachable; +function isAnySegmentReachable(segments) { + + for (const segment of segments) { + if (segment.reachable) { + return true; + } + } + + return false; } /** @@ -210,7 +217,8 @@ module.exports = { isConstructor: true, hasExtends: Boolean(superClass), superIsConstructor: isPossibleConstructor(superClass), - codePath + codePath, + currentSegments: new Set() }; } else { funcInfo = { @@ -218,7 +226,8 @@ module.exports = { isConstructor: false, hasExtends: false, superIsConstructor: false, - codePath + codePath, + currentSegments: new Set() }; } }, @@ -261,6 +270,9 @@ module.exports = { * @returns {void} */ onCodePathSegmentStart(segment) { + + funcInfo.currentSegments.add(segment); + if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { return; } @@ -281,6 +293,19 @@ module.exports = { } }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + /** * Update information of the code path segment when a code path was * looped. @@ -344,12 +369,11 @@ module.exports = { // Reports if needed. if (funcInfo.hasExtends) { - const segments = funcInfo.codePath.currentSegments; + const segments = funcInfo.currentSegments; let duplicate = false; let info = null; - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + for (const segment of segments) { if (segment.reachable) { info = segInfoMap[segment.id]; @@ -374,7 +398,7 @@ module.exports = { info.validNodes.push(node); } } - } else if (funcInfo.codePath.currentSegments.some(isReachable)) { + } else if (isAnySegmentReachable(funcInfo.currentSegments)) { context.report({ messageId: "unexpected", node @@ -398,10 +422,9 @@ module.exports = { } // Returning argument is a substitute of 'super()'. - const segments = funcInfo.codePath.currentSegments; + const segments = funcInfo.currentSegments; - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + for (const segment of segments) { if (segment.reachable) { const info = segInfoMap[segment.id]; diff --git a/lib/rules/getter-return.js b/lib/rules/getter-return.js index 622b6a7541c..79ebf3e0902 100644 --- a/lib/rules/getter-return.js +++ b/lib/rules/getter-return.js @@ -14,15 +14,23 @@ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ + const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u; /** - * Checks a given code path segment is reachable. - * @param {CodePathSegment} segment A segment to check. - * @returns {boolean} `true` if the segment is reachable. + * Checks all segments in a set and returns true if any are reachable. + * @param {Set} segments The segments to check. + * @returns {boolean} True if any segment is reachable; false otherwise. */ -function isReachable(segment) { - return segment.reachable; +function isAnySegmentReachable(segments) { + + for (const segment of segments) { + if (segment.reachable) { + return true; + } + } + + return false; } //------------------------------------------------------------------------------ @@ -71,7 +79,8 @@ module.exports = { codePath: null, hasReturn: false, shouldCheck: false, - node: null + node: null, + currentSegments: [] }; /** @@ -85,7 +94,7 @@ module.exports = { */ function checkLastSegment(node) { if (funcInfo.shouldCheck && - funcInfo.codePath.currentSegments.some(isReachable) + isAnySegmentReachable(funcInfo.currentSegments) ) { context.report({ node, @@ -144,7 +153,8 @@ module.exports = { codePath, hasReturn: false, shouldCheck: isGetter(node), - node + node, + currentSegments: new Set() }; }, @@ -152,6 +162,21 @@ module.exports = { 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); + }, // Checks the return statement is valid. ReturnStatement(node) { diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js index bd2ee9bbe2c..91da1212022 100644 --- a/lib/rules/no-fallthrough.js +++ b/lib/rules/no-fallthrough.js @@ -16,6 +16,22 @@ const { directivesPattern } = require("../shared/directives"); const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu; +/** + * Checks all segments in a set and returns true if any are reachable. + * @param {Set} 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 comment string is really a fallthrough comment and not an ESLint directive. * @param {string} comment The comment string to check. @@ -51,15 +67,6 @@ function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, f return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern)); } -/** - * Checks whether or not a given code path segment is reachable. - * @param {CodePathSegment} segment A CodePathSegment to check. - * @returns {boolean} `true` if the segment is reachable. - */ -function isReachable(segment) { - return segment.reachable; -} - /** * Checks whether a node and a token are separated by blank lines * @param {ASTNode} node The node to check @@ -109,7 +116,8 @@ module.exports = { create(context) { const options = context.options[0] || {}; - let currentCodePath = null; + const codePathSegments = []; + let currentCodePathSegments = new Set(); const sourceCode = context.sourceCode; const allowEmptyCase = options.allowEmptyCase || false; @@ -126,13 +134,33 @@ module.exports = { fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT; } return { - onCodePathStart(codePath) { - currentCodePath = codePath; + + onCodePathStart() { + codePathSegments.push(currentCodePathSegments); + currentCodePathSegments = new Set(); }, + onCodePathEnd() { - currentCodePath = currentCodePath.upper; + currentCodePathSegments = codePathSegments.pop(); + }, + + onUnreachableCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); }, + onCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + SwitchCase(node) { /* @@ -157,7 +185,7 @@ module.exports = { * `break`, `return`, or `throw` are unreachable. * And allows empty cases and the last case. */ - if (currentCodePath.currentSegments.some(isReachable) && + if (isAnySegmentReachable(currentCodePathSegments) && (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) && node.parent.cases[node.parent.cases.length - 1] !== node) { fallthroughCase = node; diff --git a/lib/rules/no-this-before-super.js b/lib/rules/no-this-before-super.js index 139bb6649d1..f96d8ace81d 100644 --- a/lib/rules/no-this-before-super.js +++ b/lib/rules/no-this-before-super.js @@ -90,6 +90,21 @@ module.exports = { return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends); } + /** + * Determines if every segment in a set has been called. + * @param {Set} segments The segments to search. + * @returns {boolean} True if every segment has been called; false otherwise. + */ + function isEverySegmentCalled(segments) { + for (const segment of segments) { + if (!isCalled(segment)) { + return false; + } + } + + return true; + } + /** * Checks whether or not this is before `super()` is called. * @returns {boolean} `true` if this is before `super()` is called. @@ -97,7 +112,7 @@ module.exports = { function isBeforeCallOfSuper() { return ( isInConstructorOfDerivedClass() && - !funcInfo.codePath.currentSegments.every(isCalled) + !isEverySegmentCalled(funcInfo.currentSegments) ); } @@ -108,11 +123,9 @@ module.exports = { * @returns {void} */ function setInvalid(node) { - const segments = funcInfo.codePath.currentSegments; - - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + const segments = funcInfo.currentSegments; + for (const segment of segments) { if (segment.reachable) { segInfoMap[segment.id].invalidNodes.push(node); } @@ -124,11 +137,9 @@ module.exports = { * @returns {void} */ function setSuperCalled() { - const segments = funcInfo.codePath.currentSegments; - - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + const segments = funcInfo.currentSegments; + for (const segment of segments) { if (segment.reachable) { segInfoMap[segment.id].superCalled = true; } @@ -156,14 +167,16 @@ module.exports = { classNode.superClass && !astUtils.isNullOrUndefined(classNode.superClass) ), - codePath + codePath, + currentSegments: new Set() }; } else { funcInfo = { upper: funcInfo, isConstructor: false, hasExtends: false, - codePath + codePath, + currentSegments: new Set() }; } }, @@ -211,6 +224,8 @@ module.exports = { * @returns {void} */ onCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + if (!isInConstructorOfDerivedClass()) { return; } @@ -225,6 +240,18 @@ module.exports = { }; }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + /** * Update information of the code path segment when a code path was * looped. diff --git a/lib/rules/no-unreachable-loop.js b/lib/rules/no-unreachable-loop.js index 1df764e17d8..577d39ac7c7 100644 --- a/lib/rules/no-unreachable-loop.js +++ b/lib/rules/no-unreachable-loop.js @@ -11,6 +11,22 @@ const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"]; +/** + * Checks all segments in a set and returns true if any are reachable. + * @param {Set} 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; +} + /** * Determines whether the given node is the first node in the code path to which a loop statement * 'loops' for the next iteration. @@ -90,29 +106,36 @@ module.exports = { loopsByTargetSegments = new Map(), loopsToReport = new Set(); - let currentCodePath = null; + const codePathSegments = []; + let currentCodePathSegments = new Set(); return { - onCodePathStart(codePath) { - currentCodePath = codePath; + + onCodePathStart() { + codePathSegments.push(currentCodePathSegments); + currentCodePathSegments = new Set(); }, onCodePathEnd() { - currentCodePath = currentCodePath.upper; + currentCodePathSegments = codePathSegments.pop(); }, - [loopSelector](node) { + onUnreachableCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); + }, - /** - * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. - * For unreachable segments, the code path analysis does not raise events required for this implementation. - */ - if (currentCodePath.currentSegments.some(segment => segment.reachable)) { - loopsToReport.add(node); - } + onUnreachableCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); }, onCodePathSegmentStart(segment, node) { + + currentCodePathSegments.add(segment); + if (isLoopingTarget(node)) { const loop = node.parent; @@ -140,6 +163,18 @@ module.exports = { } }, + [loopSelector](node) { + + /** + * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. + * For unreachable segments, the code path analysis does not raise events required for this implementation. + */ + if (isAnySegmentReachable(currentCodePathSegments)) { + loopsToReport.add(node); + } + }, + + "Program:exit"() { loopsToReport.forEach( node => context.report({ node, messageId: "invalid" }) diff --git a/lib/rules/no-unreachable.js b/lib/rules/no-unreachable.js index 6216a73a235..0cf750e4251 100644 --- a/lib/rules/no-unreachable.js +++ b/lib/rules/no-unreachable.js @@ -24,12 +24,19 @@ function isInitialized(node) { } /** - * 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} 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; } /** @@ -124,7 +131,6 @@ module.exports = { }, create(context) { - let currentCodePath = null; /** @type {ConstructorInfo | null} */ let constructorInfo = null; @@ -132,6 +138,12 @@ module.exports = { /** @type {ConsecutiveRange} */ const range = new ConsecutiveRange(context.sourceCode); + /** @type {Array>} */ + const codePathSegments = []; + + /** @type {Set} */ + let currentCodePathSegments = new Set(); + /** * Reports a given node if it's unreachable. * @param {ASTNode} node A statement node to report. @@ -140,7 +152,7 @@ module.exports = { function reportIfUnreachable(node) { let nextNode = null; - if (node && (node.type === "PropertyDefinition" || currentCodePath.currentSegments.every(isUnreachable))) { + if (node && (node.type === "PropertyDefinition" || areAllSegmentsUnreachable(currentCodePathSegments))) { // Store this statement to distinguish consecutive statements. if (range.isEmpty) { @@ -181,12 +193,29 @@ module.exports = { return { // Manages the current code path. - onCodePathStart(codePath) { - currentCodePath = codePath; + onCodePathStart() { + codePathSegments.push(currentCodePathSegments); + currentCodePathSegments = new Set(); }, onCodePathEnd() { - currentCodePath = currentCodePath.upper; + currentCodePathSegments = codePathSegments.pop(); + }, + + onUnreachableCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); }, // Registers for all statement nodes (excludes FunctionDeclaration). diff --git a/lib/rules/no-useless-return.js b/lib/rules/no-useless-return.js index f89523153d4..81d61051053 100644 --- a/lib/rules/no-useless-return.js +++ b/lib/rules/no-useless-return.js @@ -57,6 +57,22 @@ function isInFinally(node) { return false; } +/** + * Checks all segments in a set and returns true if any are reachable. + * @param {Set} 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; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -205,7 +221,6 @@ module.exports = { */ function markReturnStatementsOnCurrentSegmentsAsUsed() { scopeInfo - .codePath .currentSegments .forEach(segment => markReturnStatementsOnSegmentAsUsed(segment, new Set())); } @@ -222,7 +237,8 @@ module.exports = { upper: scopeInfo, uselessReturns: [], traversedTryBlockStatements: [], - codePath + codePath, + currentSegments: new Set() }; }, @@ -259,6 +275,9 @@ module.exports = { * NOTE: This event is notified for only reachable segments. */ onCodePathSegmentStart(segment) { + + scopeInfo.currentSegments.add(segment); + const info = { uselessReturns: getUselessReturns([], segment.allPrevSegments), returned: false @@ -268,6 +287,18 @@ module.exports = { segmentInfoMap.set(segment, info); }, + onUnreachableCodePathSegmentStart(segment) { + scopeInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + scopeInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + scopeInfo.currentSegments.delete(segment); + }, + // Adds ReturnStatement node to check whether it's useless or not. ReturnStatement(node) { if (node.argument) { @@ -279,12 +310,12 @@ module.exports = { isInFinally(node) || // Ignore `return` statements in unreachable places (https://github.com/eslint/eslint/issues/11647). - !scopeInfo.codePath.currentSegments.some(s => s.reachable) + !isAnySegmentReachable(scopeInfo.currentSegments) ) { return; } - for (const segment of scopeInfo.codePath.currentSegments) { + for (const segment of scopeInfo.currentSegments) { const info = segmentInfoMap.get(segment); if (info) { diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js index ba369a203e7..7e397ceb1cf 100644 --- a/lib/rules/require-atomic-updates.js +++ b/lib/rules/require-atomic-updates.js @@ -213,7 +213,8 @@ module.exports = { stack = { upper: stack, codePath, - referenceMap: shouldVerify ? createReferenceMap(scope) : null + referenceMap: shouldVerify ? createReferenceMap(scope) : null, + currentSegments: new Set() }; }, onCodePathEnd() { @@ -223,11 +224,25 @@ module.exports = { // Initialize the segment information. onCodePathSegmentStart(segment) { segmentInfo.initialize(segment); + stack.currentSegments.add(segment); }, + onUnreachableCodePathSegmentStart(segment) { + stack.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + stack.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + stack.currentSegments.delete(segment); + }, + + // Handle references to prepare verification. Identifier(node) { - const { codePath, referenceMap } = stack; + const { referenceMap } = stack; const reference = referenceMap && referenceMap.get(node); // Ignore if this is not a valid variable reference. @@ -240,7 +255,7 @@ module.exports = { // Add a fresh read variable. if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { - segmentInfo.markAsRead(codePath.currentSegments, variable); + segmentInfo.markAsRead(stack.currentSegments, variable); } /* @@ -267,16 +282,15 @@ module.exports = { * If the reference exists in `outdatedReadVariables` list, report it. */ ":expression:exit"(node) { - const { codePath, referenceMap } = stack; // referenceMap exists if this is in a resumable function scope. - if (!referenceMap) { + if (!stack.referenceMap) { return; } // Mark the read variables on this code path as outdated. if (node.type === "AwaitExpression" || node.type === "YieldExpression") { - segmentInfo.makeOutdated(codePath.currentSegments); + segmentInfo.makeOutdated(stack.currentSegments); } // Verify. @@ -288,7 +302,7 @@ module.exports = { for (const reference of references) { const variable = reference.resolved; - if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { + if (segmentInfo.isOutdated(stack.currentSegments, variable)) { if (node.parent.left === reference.identifier) { context.report({ node: node.parent, diff --git a/tests/lib/linter/code-path-analysis/code-path-analyzer.js b/tests/lib/linter/code-path-analysis/code-path-analyzer.js index cc2717a7ff8..dbed9b46194 100644 --- a/tests/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/linter/code-path-analysis/code-path-analyzer.js @@ -439,6 +439,164 @@ describe("CodePathAnalyzer", () => { }); }); + describe("onUnreachableCodePathSegmentStart", () => { + it("should be fired after a throw", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentStart(segment, node) { + lastCodePathNodeType = node.type; + + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "ExpressionStatement"); + }, + ExpressionStatement() { + assert.strictEqual(lastCodePathNodeType, "ExpressionStatement"); + } + }) + }); + linter.verify( + "throw 'boom'; foo();", + { rules: { test: 2 } } + ); + + }); + + it("should be fired after a return", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentStart(segment, node) { + lastCodePathNodeType = node.type; + + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "ExpressionStatement"); + }, + ExpressionStatement() { + assert.strictEqual(lastCodePathNodeType, "ExpressionStatement"); + } + }) + }); + linter.verify( + "function foo() { return; foo(); }", + { rules: { test: 2 } } + ); + + }); + }); + + describe("onUnreachableCodePathSegmentEnd", () => { + it("should be fired after a throw", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(segment, node) { + lastCodePathNodeType = node.type; + + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "Program"); + } + }) + }); + linter.verify( + "throw 'boom'; foo();", + { rules: { test: 2 } } + ); + + assert.strictEqual(lastCodePathNodeType, "Program"); + }); + + it("should be fired after a return", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(segment, node) { + lastCodePathNodeType = node.type; + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "FunctionDeclaration"); + }, + "Program:exit"() { + assert.strictEqual(lastCodePathNodeType, "FunctionDeclaration"); + } + }) + }); + linter.verify( + "function foo() { return; foo(); }", + { rules: { test: 2 } } + ); + + }); + + it("should be fired after a return inside of function and if statement", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(segment, node) { + lastCodePathNodeType = node.type; + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "BlockStatement"); + }, + "Program:exit"() { + assert.strictEqual(lastCodePathNodeType, "BlockStatement"); + } + }) + }); + linter.verify( + "function foo() { if (bar) { return; foo(); } else {} }", + { rules: { test: 2 } } + ); + + }); + + it("should be fired at the end of programs/functions for the final segment", () => { + let count = 0; + let lastNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(cp, node) { + count += 1; + + assert(cp instanceof CodePathSegment); + if (count === 4) { + assert(node.type === "Program"); + } else if (count === 1) { + assert(node.type === "FunctionDeclaration"); + } else if (count === 2) { + assert(node.type === "FunctionExpression"); + } else if (count === 3) { + assert(node.type === "ArrowFunctionExpression"); + } + assert(node.type === lastNodeType); + }, + "Program:exit"() { + lastNodeType = "Program"; + }, + "FunctionDeclaration:exit"() { + lastNodeType = "FunctionDeclaration"; + }, + "FunctionExpression:exit"() { + lastNodeType = "FunctionExpression"; + }, + "ArrowFunctionExpression:exit"() { + lastNodeType = "ArrowFunctionExpression"; + } + }) + }); + linter.verify( + "foo(); function foo() { return; } var foo = function() { return; }; var foo = () => { return; }; throw 'boom';", + { rules: { test: 2 }, env: { es6: true } } + ); + + assert(count === 4); + }); + }); + describe("onCodePathSegmentLoop", () => { it("should be fired in `while` loops", () => { let count = 0; diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index 8e1f83af616..3e34c4c34bd 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2248,6 +2248,45 @@ describe("FlatRuleTester", () => { }); }); + describe("deprecations", () => { + let processStub; + + beforeEach(() => { + processStub = sinon.stub(process, "emitWarning"); + }); + + afterEach(() => { + processStub.restore(); + }); + + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { + + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => { }); + } + }) + }; + + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "\"use-current-segments\" 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" + ] + ); + + }); + + }); + /** * Asserts that a particular value will be emitted from an EventEmitter. * @param {EventEmitter} emitter The emitter that should emit a value diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index e3c3d5a0061..68cf887adb9 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2490,6 +2490,31 @@ describe("RuleTester", () => { assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { + + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => {}); + } + }) + }; + + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "\"use-current-segments\" 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" + ] + ); + }); + Object.entries({ getSource: "getText", getSourceLines: "getLines",