Skip to content

feat: change behavior of inactive flags #19386

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

Merged
merged 3 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions docs/src/_data/flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
])
)
};
};
17 changes: 13 additions & 4 deletions docs/src/pages/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -51,11 +59,12 @@ The following flags were once used but are no longer active.
<tr>
<th>Flag</th>
<th>Description</th>
<th>Inactivity Reason</th>
</tr>
</thead>
<tbody>
{%- for name, desc in flags.inactive -%}
<tr><td><code>{{name}}</code></td><td>{{desc}}</td></tr>
{%- for name, data in flags.inactive -%}
<tr><td><code>{{name}}</code></td><td>{{data.description}}</td><td>{{data.inactivityReason}}</td></tr>
{%- endfor -%}
</tbody>
</table>
Expand Down
2 changes: 1 addition & 1 deletion lib/eslint/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
27 changes: 24 additions & 3 deletions lib/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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: [],
Expand Down
48 changes: 43 additions & 5 deletions lib/shared/flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>}
Expand All @@ -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<string, string>}
* The set of flags that used to be active.
* @type {Map<string, InactiveFlagData>}
*/
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
};
49 changes: 46 additions & 3 deletions tests/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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");
Expand Down
75 changes: 63 additions & 12 deletions tests/lib/eslint/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,33 +335,67 @@ 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", () => {

/** @type {InstanceType<ESLint>} */
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);

});

Expand All @@ -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()", () => {
Expand Down
Loading
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