Skip to content

Commit 0f49329

Browse files
authored
refactor: use a service to emit warnings (#19725)
* refactor: use a service to emit warnings * add `ConfigLoaderConstructorOptions` type * apply suggestions from code review * use preformatted message in `emitInactiveFlagWarning` * add constructor
1 parent 20a9e59 commit 0f49329

File tree

10 files changed

+331
-205
lines changed

10 files changed

+331
-205
lines changed

lib/cli.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,8 @@ const cli = {
439439
debug("Using flat config?", usingFlatConfig);
440440

441441
if (allowFlatConfig && !usingFlatConfig) {
442-
process.emitWarning(
443-
"You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.",
444-
"ESLintRCWarning",
445-
);
442+
const { WarningService } = require("./services/warning-service");
443+
new WarningService().emitESLintRCWarning();
446444
}
447445

448446
const CLIOptions = createCLIOptions(usingFlatConfig);

lib/config/config-loader.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const findUp = require("find-up");
1515
const { pathToFileURL } = require("node:url");
1616
const debug = require("debug")("eslint:config-loader");
1717
const { FlatConfigArray } = require("./flat-config-array");
18+
const { WarningService } = require("../services/warning-service");
1819

1920
//-----------------------------------------------------------------------------
2021
// Types
@@ -32,6 +33,7 @@ const { FlatConfigArray } = require("./flat-config-array");
3233
* @property {Array<string>} [ignorePatterns] The ignore patterns to use.
3334
* @property {Config|Array<Config>} [overrideConfig] The override config to use.
3435
* @property {boolean} [hasUnstableNativeNodeJsTSConfigFlag] The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled.
36+
* @property {WarningService} [warningService] The warning service to use.
3537
*/
3638

3739
//------------------------------------------------------------------------------
@@ -301,7 +303,9 @@ class ConfigLoader {
301303
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
302304
*/
303305
constructor(options) {
304-
this.#options = options;
306+
this.#options = options.warningService
307+
? options
308+
: { ...options, warningService: new WarningService() };
305309
}
306310

307311
/**
@@ -561,6 +565,7 @@ class ConfigLoader {
561565
overrideConfig,
562566
hasUnstableNativeNodeJsTSConfigFlag = false,
563567
defaultConfigs = [],
568+
warningService,
564569
} = options;
565570

566571
debug(
@@ -622,10 +627,7 @@ class ConfigLoader {
622627
}
623628

624629
if (emptyConfig) {
625-
globalThis.process?.emitWarning?.(
626-
`Running ESLint with an empty config (from ${configFilePath}). Please double-check that this is what you want. If you want to run ESLint with an empty config, export [{}] to remove this warning.`,
627-
"ESLintEmptyConfigWarning",
628-
);
630+
warningService.emitEmptyConfigWarning(configFilePath);
629631
}
630632
}
631633

@@ -713,8 +715,11 @@ class LegacyConfigLoader extends ConfigLoader {
713715
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
714716
*/
715717
constructor(options) {
716-
super(options);
717-
this.#options = options;
718+
const normalizedOptions = options.warningService
719+
? options
720+
: { ...options, warningService: new WarningService() };
721+
super(normalizedOptions);
722+
this.#options = normalizedOptions;
718723
}
719724

720725
/**

lib/eslint/eslint.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const { pathToFileURL } = require("node:url");
4040
const LintResultCache = require("../cli-engine/lint-result-cache");
4141
const { Retrier } = require("@humanwhocodes/retry");
4242
const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
43+
const { WarningService } = require("../services/warning-service");
4344

4445
/*
4546
* This is necessary to allow overwriting writeFile for testing purposes.
@@ -431,10 +432,12 @@ class ESLint {
431432
constructor(options = {}) {
432433
const defaultConfigs = [];
433434
const processedOptions = processOptions(options);
435+
const warningService = new WarningService();
434436
const linter = new Linter({
435437
cwd: processedOptions.cwd,
436438
configType: "flat",
437439
flags: mergeEnvironmentFlags(processedOptions.flags),
440+
warningService,
438441
});
439442

440443
const cacheFilePath = getCacheFile(
@@ -457,6 +460,7 @@ class ESLint {
457460
hasUnstableNativeNodeJsTSConfigFlag: linter.hasFlag(
458461
"unstable_native_nodejs_ts_config",
459462
),
463+
warningService,
460464
};
461465

462466
this.#configLoader = linter.hasFlag("unstable_config_lookup_from_file")
@@ -496,10 +500,7 @@ class ESLint {
496500

497501
// Check for the .eslintignore file, and warn if it's present.
498502
if (existsSync(path.resolve(processedOptions.cwd, ".eslintignore"))) {
499-
process.emitWarning(
500-
'The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files',
501-
"ESLintIgnoreWarning",
502-
);
503+
warningService.emitESLintIgnoreWarning();
503504
}
504505
}
505506

lib/linter/linter.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const { FileContext } = require("./file-context");
6565
const { ProcessorService } = require("../services/processor-service");
6666
const { containsDifferentProperty } = require("../shared/option-utils");
6767
const { Config } = require("../config/config");
68+
const { WarningService } = require("../services/warning-service");
6869
const STEP_KIND_VISIT = 1;
6970
const STEP_KIND_CALL = 2;
7071

@@ -113,6 +114,7 @@ const STEP_KIND_CALL = 2;
113114
* @property {Map<string, Parser>} parserMap The loaded parsers.
114115
* @property {{ passes: TimePass[]; }} times The times spent on applying a rule to a file (see `stats` option).
115116
* @property {Rules} ruleMap The loaded rules.
117+
* @property {WarningService} warningService The warning service.
116118
*/
117119

118120
/**
@@ -1483,31 +1485,33 @@ class Linter {
14831485
* @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
14841486
* @param {Array<string>} [config.flags] the feature flags to enable.
14851487
* @param {"flat"|"eslintrc"} [config.configType="flat"] the type of config used.
1488+
* @param {WarningService} [config.warningService] The warning service to use.
14861489
*/
1487-
constructor({ cwd, configType = "flat", flags = [] } = {}) {
1490+
constructor({
1491+
cwd,
1492+
configType = "flat",
1493+
flags = [],
1494+
warningService = new WarningService(),
1495+
} = {}) {
14881496
const processedFlags = [];
14891497

14901498
flags.forEach(flag => {
14911499
if (inactiveFlags.has(flag)) {
14921500
const inactiveFlagData = inactiveFlags.get(flag);
14931501
const inactivityReason =
14941502
getInactivityReasonMessage(inactiveFlagData);
1503+
const message = `The flag '${flag}' is inactive: ${inactivityReason}`;
14951504

14961505
if (typeof inactiveFlagData.replacedBy === "undefined") {
1497-
throw new Error(
1498-
`The flag '${flag}' is inactive: ${inactivityReason}`,
1499-
);
1506+
throw new Error(message);
15001507
}
15011508

15021509
// if there's a replacement, enable it instead of original
15031510
if (typeof inactiveFlagData.replacedBy === "string") {
15041511
processedFlags.push(inactiveFlagData.replacedBy);
15051512
}
15061513

1507-
globalThis.process?.emitWarning?.(
1508-
`The flag '${flag}' is inactive: ${inactivityReason}`,
1509-
`ESLintInactiveFlag_${flag}`,
1510-
);
1514+
warningService.emitInactiveFlagWarning(flag, message);
15111515

15121516
return;
15131517
}
@@ -1528,6 +1532,7 @@ class Linter {
15281532
configType, // TODO: Remove after flat config conversion
15291533
parserMap: new Map([["espree", espree]]),
15301534
ruleMap: new Rules(),
1535+
warningService,
15311536
});
15321537

15331538
this.version = pkg.version;
@@ -2710,6 +2715,14 @@ class Linter {
27102715
options && typeof options.fix !== "undefined" ? options.fix : true;
27112716
const stats = options?.stats;
27122717

2718+
const slots = internalSlotsMap.get(this);
2719+
2720+
// Remove lint times from the last run.
2721+
if (stats) {
2722+
delete slots.times;
2723+
slots.fixPasses = 0;
2724+
}
2725+
27132726
/**
27142727
* This loop continues until one of the following is true:
27152728
*
@@ -2719,14 +2732,6 @@ class Linter {
27192732
* That means anytime a fix is successfully applied, there will be another pass.
27202733
* Essentially, guaranteeing a minimum of two passes.
27212734
*/
2722-
const slots = internalSlotsMap.get(this);
2723-
2724-
// Remove lint times from the last run.
2725-
if (stats) {
2726-
delete slots.times;
2727-
slots.fixPasses = 0;
2728-
}
2729-
27302735
do {
27312736
passNumber++;
27322737
let tTotal;
@@ -2798,9 +2803,8 @@ class Linter {
27982803
debug(
27992804
`Circular fixes detected after pass ${passNumber}. Exiting fix loop.`,
28002805
);
2801-
globalThis?.process?.emitWarning?.(
2802-
`Circular fixes detected while fixing ${options?.filename ?? "text"}. It is likely that you have conflicting rules in your configuration.`,
2803-
"ESLintCircularFixesWarning",
2806+
slots.warningService.emitCircularFixesWarning(
2807+
options?.filename ?? "text",
28042808
);
28052809
break;
28062810
}

lib/services/warning-service.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* @fileoverview Emits warnings for ESLint.
3+
* @author Francesco Trotta
4+
*/
5+
6+
"use strict";
7+
8+
//-----------------------------------------------------------------------------
9+
// Exports
10+
//-----------------------------------------------------------------------------
11+
12+
/**
13+
* A service that emits warnings for ESLint.
14+
*/
15+
class WarningService {
16+
/**
17+
* Creates a new instance of the service.
18+
* @param {{ emitWarning?: ((warning: string, type: string) => void) | undefined }} [options] A function called internally to emit warnings using API provided by the runtime.
19+
*/
20+
constructor({
21+
emitWarning = globalThis.process?.emitWarning ?? (() => {}),
22+
} = {}) {
23+
this.emitWarning = emitWarning;
24+
}
25+
26+
/**
27+
* Emits a warning when circular fixes are detected while fixing a file.
28+
* This method is used by the Linter and is safe to call outside Node.js.
29+
* @param {string} filename The name of the file being fixed.
30+
* @returns {void}
31+
*/
32+
emitCircularFixesWarning(filename) {
33+
this.emitWarning(
34+
`Circular fixes detected while fixing ${filename}. It is likely that you have conflicting rules in your configuration.`,
35+
"ESLintCircularFixesWarning",
36+
);
37+
}
38+
39+
/**
40+
* Emits a warning when an empty config file has been loaded.
41+
* @param {string} configFilePath The path to the config file.
42+
* @returns {void}
43+
*/
44+
emitEmptyConfigWarning(configFilePath) {
45+
this.emitWarning(
46+
`Running ESLint with an empty config (from ${configFilePath}). Please double-check that this is what you want. If you want to run ESLint with an empty config, export [{}] to remove this warning.`,
47+
"ESLintEmptyConfigWarning",
48+
);
49+
}
50+
51+
/**
52+
* Emits a warning when an ".eslintignore" file is found.
53+
* @returns {void}
54+
*/
55+
emitESLintIgnoreWarning() {
56+
this.emitWarning(
57+
'The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files',
58+
"ESLintIgnoreWarning",
59+
);
60+
}
61+
62+
/**
63+
* Emits a warning when the ESLINT_USE_FLAT_CONFIG environment variable is set to "false".
64+
* @returns {void}
65+
*/
66+
emitESLintRCWarning() {
67+
this.emitWarning(
68+
"You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.",
69+
"ESLintRCWarning",
70+
);
71+
}
72+
73+
/**
74+
* Emits a warning when an inactive flag is used.
75+
* This method is used by the Linter and is safe to call outside Node.js.
76+
* @param {string} flag The name of the flag.
77+
* @param {string} message The warning message.
78+
* @returns {void}
79+
*/
80+
emitInactiveFlagWarning(flag, message) {
81+
this.emitWarning(message, `ESLintInactiveFlag_${flag}`);
82+
}
83+
}
84+
85+
module.exports = { WarningService };

tests/lib/cli.js

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const assert = require("chai").assert,
2222
sinon = require("sinon"),
2323
fs = require("node:fs"),
2424
os = require("node:os"),
25-
sh = require("shelljs");
25+
sh = require("shelljs"),
26+
{ WarningService } = require("../../lib/services/warning-service");
2627

2728
const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
2829

@@ -147,11 +148,8 @@ describe("cli", () => {
147148
});
148149

149150
beforeEach(() => {
150-
sinon
151-
.stub(process, "emitWarning")
152-
.withArgs(sinon.match.any, "ESLintIgnoreWarning")
153-
.returns();
154-
process.emitWarning.callThrough();
151+
// Silence ".eslintignore" warnings for tests
152+
sinon.stub(WarningService.prototype, "emitESLintIgnoreWarning");
155153
});
156154

157155
afterEach(() => {
@@ -224,16 +222,19 @@ describe("cli", () => {
224222
const originalEnv = process.env;
225223
const originalCwd = process.cwd;
226224

227-
let processStub;
225+
let emitESLintRCWarningStub;
228226

229227
beforeEach(() => {
230228
sinon.restore();
231-
processStub = sinon.stub(process, "emitWarning");
229+
emitESLintRCWarningStub = sinon.stub(
230+
WarningService.prototype,
231+
"emitESLintRCWarning",
232+
);
232233
process.env = { ...originalEnv };
233234
});
234235

235236
afterEach(() => {
236-
processStub.restore();
237+
emitESLintRCWarningStub.restore();
237238
process.env = originalEnv;
238239
process.cwd = originalCwd;
239240
});
@@ -264,14 +265,9 @@ describe("cli", () => {
264265
assert.strictEqual(exitCode, 0);
265266

266267
if (useFlatConfig) {
267-
assert.strictEqual(
268-
processStub.callCount,
269-
1,
270-
"calls `process.emitWarning()` once",
271-
);
272-
assert.strictEqual(
273-
processStub.getCall(0).args[1],
274-
"ESLintRCWarning",
268+
assert(
269+
emitESLintRCWarningStub.calledOnce,
270+
"calls `warningService.emitESLintRCWarning()` once",
275271
);
276272
}
277273
});

0 commit comments

Comments
 (0)