diff --git a/docs/src/extend/code-path-analysis.md b/docs/src/extend/code-path-analysis.md index 36d827bee774..879119574907 100644 --- a/docs/src/extend/code-path-analysis.md +++ b/docs/src/extend/code-path-analysis.md @@ -37,6 +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[]`) - **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. @@ -634,7 +635,7 @@ last(); ``` If there is not `catch` block, `finally` block has two current segments. -At this time when running the previous example to find unreachable nodes, `currentSegments.length` is `2`. +At this time, `CodePath.currentSegments.length` is `2`. One is the normal path, and another is the leaving path (`throw` or `return`). :::img-container diff --git a/lib/linter/code-path-analysis/code-path.js b/lib/linter/code-path-analysis/code-path.js index 90cafef68f0b..3bf570d754bf 100644 --- a/lib/linter/code-path-analysis/code-path.js +++ b/lib/linter/code-path-analysis/code-path.js @@ -125,6 +125,20 @@ class CodePath { return this.internal.thrownForkContext; } + /** + * Tracks the traversal of the code path through each segment. This array + * starts empty and segments are added or removed as the code path is + * traversed. This array always ends up empty at the end of a code path + * traversal. The `CodePathState` uses this to track its progress through + * the code path. + * This is a passthrough to the underlying `CodePathState`. + * @type {CodePathSegment[]} + * @deprecated + */ + get currentSegments() { + return this.internal.currentSegments; + } + /** * Traverses all segments in this code path. * diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 5e08eab93fdc..0a127c749a00 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -16,7 +16,8 @@ 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,14 +275,18 @@ function wrapParser(parser) { } /** - * Function to replace `SourceCode.prototype.getComments`. + * Emit a deprecation warning if rule uses CodePath#currentSegments. + * @param {string} ruleName Name of the rule. * @returns {void} - * @throws {Error} Deprecation message. */ -function getCommentsDeprecation() { - throw new Error( - "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead." - ); +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" + ); + } } /** @@ -711,11 +716,17 @@ class FlatRuleTester { } // Verify the code. - const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const { applyLanguageOptions, applyInlineConfig, finalize } = 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); + } + }); forbiddenMethods.forEach(methodName => { SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype); @@ -723,7 +734,7 @@ class FlatRuleTester { messages = linter.verify(code, configs, filename); } finally { - SourceCode.prototype.getComments = getComments; + Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; SourceCode.prototype.finalize = finalize; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 74ca7c11493d..bd46fcc807a5 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 }); @@ -344,37 +345,20 @@ function emitMissingSchemaWarning(ruleName) { } /** - * Emit a deprecation warning if a rule uses a deprecated `context` method. + * Emit a deprecation warning if rule uses CodePath#currentSegments. * @param {string} ruleName Name of the rule. - * @param {string} methodName The name of the method on `context` that was used. * @returns {void} */ -function emitDeprecatedContextMethodWarning(ruleName, methodName) { - if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { - emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; +function emitCodePathCurrentSegmentsWarning(ruleName) { + if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { + emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; process.emitWarning( - `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, + `"${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" ); } } -/** - * Emit a deprecation warning if `context.parserServices` is used. - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitParserServicesWarning(ruleName) { - if (!emitParserServicesWarning[`warned-${ruleName}`]) { - emitParserServicesWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, - "DeprecationWarning" - ); - } -} - - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -725,11 +709,17 @@ class RuleTester { validate(config, "rule-tester", id => (id === ruleName ? rule : null)); // Verify the code. - const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const { applyLanguageOptions, applyInlineConfig, finalize } = 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); + } + }); forbiddenMethods.forEach(methodName => { SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName); @@ -737,7 +727,7 @@ class RuleTester { messages = linter.verify(code, config, filename); } finally { - SourceCode.prototype.getComments = getComments; + Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; SourceCode.prototype.finalize = finalize; 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 e580c14d1344..dbed9b46194f 100644 --- a/tests/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/linter/code-path-analysis/code-path-analyzer.js @@ -137,6 +137,37 @@ describe("CodePathAnalyzer", () => { assert(actual[1].thrownSegments[0] instanceof CodePathSegment); }); + it("should have `currentSegments` as CodePathSegment[]", () => { + assert(Array.isArray(actual[0].currentSegments)); + assert(Array.isArray(actual[1].currentSegments)); + assert(actual[0].currentSegments.length === 0); + assert(actual[1].currentSegments.length === 0); + + // there is the current segment in progress. + linter.defineRule("test", { + create() { + let codePath = null; + + return { + onCodePathStart(cp) { + codePath = cp; + }, + ReturnStatement() { + assert(codePath.currentSegments.length === 1); + assert(codePath.currentSegments[0] instanceof CodePathSegment); + }, + ThrowStatement() { + assert(codePath.currentSegments.length === 1); + assert(codePath.currentSegments[0] instanceof CodePathSegment); + } + }; + } + }); + linter.verify( + "function foo(a) { if (a) return 0; else throw new Error(); }", + { rules: { test: 2 } } + ); + }); }); describe("interface of code path segments", () => { diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index d5d11e843339..5f47ce1fe4b4 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2399,6 +2399,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 ba253da8f006..5af50dd7dea5 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2490,117 +2490,31 @@ describe("RuleTester", () => { assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("should pass-through services from parseForESLint to the rule and log deprecation notice", () => { - const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); - const disallowHiRule = { - create: context => ({ - Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - - const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { - if (node.value === disallowed) { - context.report({ node, message: `Don't use '${disallowed}'` }); - } + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => {}); } }) }; - ruleTester.run("no-hi", disallowHiRule, { - valid: [ - { - code: "'Hello!'", - parser: enhancedParserPath - } - ], - invalid: [ - { - code: "'Hi!'", - parser: enhancedParserPath, - errors: [{ message: "Don't use 'Hi!'" }] - } - ] + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] }); assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); assert.deepStrictEqual( processStub.getCall(0).args, [ - "\"no-hi\" rule is using `context.parserServices`, which is deprecated and will be removed in ESLint v9. Please use `sourceCode.parserServices` instead.", + "\"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", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween", - getScope: "getScope", - getAncestors: "getAncestors", - getDeclaredVariables: "getDeclaredVariables", - markVariableAsUsed: "markVariableAsUsed" - }).forEach(([methodName, replacementName]) => { - - it(`should log a deprecation warning when calling \`context.${methodName}\``, () => { - const ruleToCheckDeprecation = { - meta: { - type: "problem", - schema: [] - }, - create(context) { - return { - Program(node) { - - // special case - if (methodName === "getTokensBetween") { - context[methodName](node, node); - } else { - context[methodName](node); - } - - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("deprecated-method", ruleToCheckDeprecation, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - `"deprecated-method" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${replacementName}()\` instead.`, - "DeprecationWarning" - ] - ); - }); - }); - }); /**