diff --git a/docs/src/_data/flags.js b/docs/src/_data/flags.js index 61966c9d6672..ec697b7088e4 100644 --- a/docs/src/_data/flags.js +++ b/docs/src/_data/flags.js @@ -5,16 +5,42 @@ "use strict"; +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Determines whether the flag is used for test purposes only. + * @param {string} name The flag name to check. + * @returns {boolean} `true` if the flag is used for test purposes only. + */ +function isTestOnlyFlag(name) { + return name.startsWith("test_only"); +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- module.exports = function() { - const { activeFlags, inactiveFlags } = require("../../../lib/shared/flags"); + const { activeFlags, inactiveFlags, getInactivityReasonMessage } = require("../../../lib/shared/flags"); return { - active: Object.fromEntries([...activeFlags]), - inactive: Object.fromEntries([...inactiveFlags]) + active: Object.fromEntries( + [...activeFlags] + .filter(([name]) => !isTestOnlyFlag(name)) + ), + inactive: Object.fromEntries( + [...inactiveFlags] + .filter(([name]) => !isTestOnlyFlag(name)) + .map(([name, inactiveFlagData]) => [ + name, + { + ...inactiveFlagData, + inactivityReason: getInactivityReasonMessage(inactiveFlagData) + } + ]) + ) }; }; diff --git a/docs/src/pages/flags.md b/docs/src/pages/flags.md index 74f7b00ebdd7..feb23fc1d090 100644 --- a/docs/src/pages/flags.md +++ b/docs/src/pages/flags.md @@ -20,9 +20,17 @@ ESLint ships experimental and future breaking changes behind feature flags to le The prefix of a flag indicates its status: * `unstable_` indicates that the feature is experimental and the implementation may change before the feature is stabilized. This is a "use at your own risk" feature. -* `v##_` indicates that the feature is stabilized and will be available in the next major release. For example, `v10_some_feature` indicates that this is a breaking change that will be formally released in ESLint v10.0.0. These flags are removed each major release. +* `v##_` indicates that the feature is stabilized and will be available in the next major release. For example, `v10_some_feature` indicates that this is a breaking change that will be formally released in ESLint v10.0.0. These flags are removed each major release, and further use of them throws an error. -A feature may move from unstable to stable without a major release if it is a non-breaking change. +A feature may move from unstable to being enabled by default without a major release if it is a non-breaking change. + +The following policies apply to `unstable_` flags. + +* When the feature is stabilized + * If enabling the feature by default would be a breaking change, a new `v##_` flag is added as active, and the `unstable_` flag becomes inactive. Further use of the `unstable_` flag automatically enables the `v##_` flag but emits a warning. + * Otherwise, the feature is enabled by default, and the `unstable_` flag becomes inactive. Further use of the `unstable_` flag emits a warning. +* If the feature is abandoned, the `unstable_` flag becomes inactive. Further use of it throws an error. +* All inactive `unstable_` flags are removed each major release, and further use of them throws an error. ## Active Flags @@ -51,11 +59,12 @@ The following flags were once used but are no longer active. Flag Description + Inactivity Reason -{%- for name, desc in flags.inactive -%} - {{name}}{{desc}} +{%- for name, data in flags.inactive -%} + {{name}}{{data.description}}{{data.inactivityReason}} {%- endfor -%} diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 89583d53f59d..a74d0da7adc1 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -470,7 +470,7 @@ class ESLint { defaultConfigs }; - this.#configLoader = processedOptions.flags.includes("unstable_config_lookup_from_file") + this.#configLoader = linter.hasFlag("unstable_config_lookup_from_file") ? new ConfigLoader(configLoaderOptions) : new LegacyConfigLoader(configLoaderOptions); diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 1baa0b8bf350..6db9c13b706a 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -43,7 +43,7 @@ const { assertIsRuleSeverity } = require("../config/flat-config-schema"); const { normalizeSeverityToString, normalizeSeverityToNumber } = require("../shared/severity"); const { deepMergeArrays } = require("../shared/deep-merge-arrays"); const jslang = require("../languages/js"); -const { activeFlags, inactiveFlags } = require("../shared/flags"); +const { activeFlags, inactiveFlags, getInactivityReasonMessage } = require("../shared/flags"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; @@ -1326,19 +1326,40 @@ class Linter { */ constructor({ cwd, configType = "flat", flags = [] } = {}) { + const processedFlags = []; + flags.forEach(flag => { if (inactiveFlags.has(flag)) { - throw new Error(`The flag '${flag}' is inactive: ${inactiveFlags.get(flag)}`); + const inactiveFlagData = inactiveFlags.get(flag); + const inactivityReason = getInactivityReasonMessage(inactiveFlagData); + + if (typeof inactiveFlagData.replacedBy === "undefined") { + throw new Error(`The flag '${flag}' is inactive: ${inactivityReason}`); + } + + // if there's a replacement, enable it instead of original + if (typeof inactiveFlagData.replacedBy === "string") { + processedFlags.push(inactiveFlagData.replacedBy); + } + + globalThis.process?.emitWarning?.( + `The flag '${flag}' is inactive: ${inactivityReason}`, + `ESLintInactiveFlag_${flag}` + ); + + return; } if (!activeFlags.has(flag)) { throw new Error(`Unknown flag '${flag}'.`); } + + processedFlags.push(flag); }); internalSlotsMap.set(this, { cwd: normalizeCwd(cwd), - flags, + flags: processedFlags, lastConfigArray: null, lastSourceCode: null, lastSuppressedMessages: [], diff --git a/lib/shared/flags.js b/lib/shared/flags.js index ba2ea2504106..7a232d2b1539 100644 --- a/lib/shared/flags.js +++ b/lib/shared/flags.js @@ -4,6 +4,23 @@ "use strict"; +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** + * @typedef {Object} InactiveFlagData + * @property {string} description Flag description + * @property {string | null} [replacedBy] Can be either: + * - An active flag (string) that enables the same feature. + * - `null` if the feature is now enabled by default. + * - Omitted if the feature has been abandoned. + */ + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + /** * The set of flags that change ESLint behavior with a description. * @type {Map} @@ -14,15 +31,36 @@ const activeFlags = new Map([ ]); /** - * The set of flags that used to be active but no longer have an effect. - * @type {Map} + * The set of flags that used to be active. + * @type {Map} */ const inactiveFlags = new Map([ - ["test_only_old", "Used only for testing."], - ["unstable_ts_config", "This flag is no longer required to enable TypeScript configuration files."] + ["test_only_replaced", { description: "Used only for testing flags that have been replaced by other flags.", replacedBy: "test_only" }], + ["test_only_enabled_by_default", { description: "Used only for testing flags whose features have been enabled by default.", replacedBy: null }], + ["test_only_abandoned", { description: "Used only for testing flags whose features have been abandoned." }], + ["unstable_ts_config", { description: "Enable TypeScript configuration files.", replacedBy: null }] ]); +/** + * Creates a message that describes the reason the flag is inactive. + * @param {InactiveFlagData} inactiveFlagData Data for the inactive flag. + * @returns {string} Message describing the reason the flag is inactive. + */ +function getInactivityReasonMessage({ replacedBy }) { + if (typeof replacedBy === "undefined") { + return "This feature has been abandoned."; + } + + if (typeof replacedBy === "string") { + return `This flag has been renamed '${replacedBy}' to reflect its stabilization. Please use '${replacedBy}' instead.`; + } + + // null + return "This feature is now enabled by default."; +} + module.exports = { activeFlags, - inactiveFlags + inactiveFlags, + getInactivityReasonMessage }; diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 34d303c8d86b..a509428b9381 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -1907,14 +1907,21 @@ describe("cli", () => { describe("--flag option", () => { - it("should throw an error when an inactive flag is used", async () => { + let processStub; + + beforeEach(() => { + sinon.restore(); + processStub = sinon.stub(process, "emitWarning").withArgs(sinon.match.any, sinon.match(/^ESLintInactiveFlag_/u)).returns(); + }); + + it("should throw an error when an inactive flag whose feature has been abandoned is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); - const input = `--flag test_only_old --config ${configPath} ${filePath}`; + const input = `--flag test_only_abandoned --config ${configPath} ${filePath}`; await stdAssert.rejects(async () => { await cli.execute(input, null, true); - }, /The flag 'test_only_old' is inactive: Used only for testing\./u); + }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u); }); it("should error out when an unknown flag is used", async () => { @@ -1927,6 +1934,42 @@ describe("cli", () => { }, /Unknown flag 'test_only_oldx'\./u); }); + it("should emit a warning and not error out when an inactive flag that has been replaced by another flag is used", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--flag test_only_replaced --config ${configPath} ${filePath}`; + const exitCode = await cli.execute(input, null, true); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'test_only_replaced' is inactive: This flag has been renamed 'test_only' to reflect its stabilization. Please use 'test_only' instead.", + "ESLintInactiveFlag_test_only_replaced" + ] + ); + sinon.assert.notCalled(log.error); + assert.strictEqual(exitCode, 0); + }); + + it("should emit a warning and not error out when an inactive flag whose feature is enabled by default is used", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--flag test_only_enabled_by_default --config ${configPath} ${filePath}`; + const exitCode = await cli.execute(input, null, true); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'test_only_enabled_by_default' is inactive: This feature is now enabled by default.", + "ESLintInactiveFlag_test_only_enabled_by_default" + ] + ); + sinon.assert.notCalled(log.error); + assert.strictEqual(exitCode, 0); + }); + it("should not error when a valid flag is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 2bcccc7424f2..15a37b86b2de 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -335,15 +335,6 @@ describe("ESLint", () => { processStub.restore(); }); - - it("should throw an error if the flag 'unstable_ts_config' is used", () => { - assert.throws( - () => new ESLint({ - flags: [...flags, "unstable_ts_config"] - }), - { message: "The flag 'unstable_ts_config' is inactive: This flag is no longer required to enable TypeScript configuration files." } - ); - }); }); describe("hasFlag", () => { @@ -351,17 +342,60 @@ describe("ESLint", () => { /** @type {InstanceType} */ let eslint; + let processStub; + + beforeEach(() => { + sinon.restore(); + processStub = sinon.stub(process, "emitWarning").withArgs(sinon.match.any, sinon.match(/^ESLintInactiveFlag_/u)).returns(); + }); + it("should return true if the flag is present and active", () => { eslint = new ESLint({ cwd: getFixturePath(), flags: ["test_only"] }); assert.strictEqual(eslint.hasFlag("test_only"), true); }); - it("should throw an error if the flag is inactive", () => { + it("should return true for the replacement flag if an inactive flag that has been replaced is used", () => { + eslint = new ESLint({ cwd: getFixturePath(), flags: ["test_only_replaced"] }); + + assert.strictEqual(eslint.hasFlag("test_only"), true); + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'test_only_replaced' is inactive: This flag has been renamed 'test_only' to reflect its stabilization. Please use 'test_only' instead.", + "ESLintInactiveFlag_test_only_replaced" + ] + ); + }); + + it("should return false if an inactive flag whose feature is enabled by default is used", () => { + eslint = new ESLint({ cwd: getFixturePath(), flags: ["test_only_enabled_by_default"] }); + + assert.strictEqual(eslint.hasFlag("test_only_enabled_by_default"), false); + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'test_only_enabled_by_default' is inactive: This feature is now enabled by default.", + "ESLintInactiveFlag_test_only_enabled_by_default" + ] + ); + }); + + it("should throw an error if an inactive flag whose feature has been abandoned is used", () => { + + assert.throws(() => { + eslint = new ESLint({ cwd: getFixturePath(), flags: ["test_only_abandoned"] }); + }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned/u); + + }); + + it("should throw an error if the flag is unknown", () => { assert.throws(() => { - eslint = new ESLint({ cwd: getFixturePath(), flags: ["test_only_old"] }); - }, /The flag 'test_only_old' is inactive/u); + eslint = new ESLint({ cwd: getFixturePath(), flags: ["foo_bar"] }); + }, /Unknown flag 'foo_bar'/u); }); @@ -370,6 +404,23 @@ describe("ESLint", () => { assert.strictEqual(eslint.hasFlag("x_feature"), false); }); + + // TODO: Remove in ESLint v10 when the flag is removed + it("should not throw an error if the flag 'unstable_ts_config' is used", () => { + eslint = new ESLint({ + flags: [...flags, "unstable_ts_config"] + }); + + assert.strictEqual(eslint.hasFlag("unstable_ts_config"), false); + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'unstable_ts_config' is inactive: This feature is now enabled by default.", + "ESLintInactiveFlag_unstable_ts_config" + ] + ); + }); }); describe("lintText()", () => { diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index a112a6089305..e14a5af1ce2c 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -8035,6 +8035,20 @@ describe("Linter with FlatConfigArray", () => { describe("hasFlag()", () => { + let processStub; + + beforeEach(() => { + + // in the browser test, `process.emitWarning` is not defined + if (typeof process !== "undefined" && typeof process.emitWarning !== "undefined") { + processStub = sinon.stub(process, "emitWarning").withArgs(sinon.match.any, sinon.match(/^ESLintInactiveFlag_/u)).returns(); + } + }); + + afterEach(() => { + sinon.restore(); + }); + it("should return true if an active flag is present", () => { assert.strictEqual( new Linter({ configType: "flat", flags: ["test_only"] }).hasFlag("test_only"), @@ -8042,11 +8056,47 @@ describe("Linter with FlatConfigArray", () => { ); }); - it("should throw an error if an inactive flag is present", () => { + it("should return true for the replacement flag if an inactive flag that has been replaced is used", () => { + assert.strictEqual( + new Linter({ configType: "flat", flags: ["test_only_replaced"] }).hasFlag("test_only"), + true + ); + + if (processStub) { + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'test_only_replaced' is inactive: This flag has been renamed 'test_only' to reflect its stabilization. Please use 'test_only' instead.", + "ESLintInactiveFlag_test_only_replaced" + ] + ); + } + }); + + it("should return false if an inactive flag whose feature is enabled by default is used", () => { + assert.strictEqual( + new Linter({ configType: "flat", flags: ["test_only_enabled_by_default"] }).hasFlag("test_only_enabled_by_default"), + false + ); + + if (processStub) { + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` for flags once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "The flag 'test_only_enabled_by_default' is inactive: This feature is now enabled by default.", + "ESLintInactiveFlag_test_only_enabled_by_default" + ] + ); + } + }); + + it("should throw an error if an inactive flag whose feature has been abandoned is used", () => { assert.throws(() => { // eslint-disable-next-line no-new -- needed for test - new Linter({ configType: "flat", flags: ["test_only_old"] }); - }, /The flag 'test_only_old' is inactive: Used only for testing/u); + new Linter({ configType: "flat", flags: ["test_only_abandoned"] }); + }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned/u); }); it("should throw an error if an unknown flag is present", () => { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy