eslint 9.27.0 → 9.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -439,10 +439,8 @@ const cli = {
439
439
  debug("Using flat config?", usingFlatConfig);
440
440
 
441
441
  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();
446
444
  }
447
445
 
448
446
  const CLIOptions = createCLIOptions(usingFlatConfig);
@@ -736,17 +734,21 @@ const cli = {
736
734
  );
737
735
  }
738
736
 
739
- const unusedSuppressionsCount =
740
- Object.keys(unusedSuppressions).length;
737
+ if (!options.passOnUnprunedSuppressions) {
738
+ const unusedSuppressionsCount =
739
+ Object.keys(unusedSuppressions).length;
741
740
 
742
- if (unusedSuppressionsCount > 0) {
743
- log.error(
744
- "There are suppressions left that do not occur anymore. Consider re-running the command with `--prune-suppressions`.",
745
- );
746
- debug(JSON.stringify(unusedSuppressions, null, 2));
741
+ if (unusedSuppressionsCount > 0) {
742
+ log.error(
743
+ "There are suppressions left that do not occur anymore. Consider re-running the command with `--prune-suppressions`.",
744
+ );
745
+ debug(JSON.stringify(unusedSuppressions, null, 2));
746
+
747
+ return 2;
748
+ }
747
749
  }
748
750
 
749
- if (shouldExitForFatalErrors || unusedSuppressionsCount > 0) {
751
+ if (shouldExitForFatalErrors) {
750
752
  return 2;
751
753
  }
752
754
 
@@ -15,6 +15,7 @@ const findUp = require("find-up");
15
15
  const { pathToFileURL } = require("node:url");
16
16
  const debug = require("debug")("eslint:config-loader");
17
17
  const { FlatConfigArray } = require("./flat-config-array");
18
+ const { WarningService } = require("../services/warning-service");
18
19
 
19
20
  //-----------------------------------------------------------------------------
20
21
  // Types
@@ -32,6 +33,7 @@ const { FlatConfigArray } = require("./flat-config-array");
32
33
  * @property {Array<string>} [ignorePatterns] The ignore patterns to use.
33
34
  * @property {Config|Array<Config>} [overrideConfig] The override config to use.
34
35
  * @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.
35
37
  */
36
38
 
37
39
  //------------------------------------------------------------------------------
@@ -137,12 +139,13 @@ function isNativeTypeScriptSupportEnabled() {
137
139
  * @since 9.24.0
138
140
  */
139
141
  async function loadTypeScriptConfigFileWithJiti(filePath, fileURL, mtime) {
140
- // eslint-disable-next-line no-use-before-define -- `ConfigLoader.loadJiti` can be overwritten for testing
141
- const { createJiti } = await ConfigLoader.loadJiti().catch(() => {
142
- throw new Error(
143
- "The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.",
144
- );
145
- });
142
+ const { createJiti, version: jitiVersion } =
143
+ // eslint-disable-next-line no-use-before-define -- `ConfigLoader.loadJiti` can be overwritten for testing
144
+ await ConfigLoader.loadJiti().catch(() => {
145
+ throw new Error(
146
+ "The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.",
147
+ );
148
+ });
146
149
 
147
150
  // `createJiti` was added in jiti v2.
148
151
  if (typeof createJiti !== "function") {
@@ -155,11 +158,15 @@ async function loadTypeScriptConfigFileWithJiti(filePath, fileURL, mtime) {
155
158
  * Disabling `moduleCache` allows us to reload a
156
159
  * config file when the last modified timestamp changes.
157
160
  */
158
-
159
- const jiti = createJiti(__filename, {
161
+ const jitiOptions = {
160
162
  moduleCache: false,
161
- interopDefault: false,
162
- });
163
+ };
164
+
165
+ if (jitiVersion.startsWith("2.1.")) {
166
+ jitiOptions.interopDefault = false;
167
+ }
168
+
169
+ const jiti = createJiti(__filename, jitiOptions);
163
170
  const config = await jiti.import(fileURL.href);
164
171
 
165
172
  importedConfigFileModificationTime.set(filePath, mtime);
@@ -301,7 +308,9 @@ class ConfigLoader {
301
308
  * @param {ConfigLoaderOptions} options The options to use when loading configuration files.
302
309
  */
303
310
  constructor(options) {
304
- this.#options = options;
311
+ this.#options = options.warningService
312
+ ? options
313
+ : { ...options, warningService: new WarningService() };
305
314
  }
306
315
 
307
316
  /**
@@ -494,11 +503,12 @@ class ConfigLoader {
494
503
 
495
504
  /**
496
505
  * Used to import the jiti dependency. This method is exposed internally for testing purposes.
497
- * @returns {Promise<Record<string, unknown>>} A promise that fulfills with a module object
498
- * or rejects with an error if jiti is not found.
506
+ * @returns {Promise<{createJiti: Function|undefined, version: string;}>} A promise that fulfills with an object containing the jiti module's createJiti function and version.
499
507
  */
500
- static loadJiti() {
501
- return import("jiti");
508
+ static async loadJiti() {
509
+ const { createJiti } = await import("jiti");
510
+ const version = require("jiti/package.json").version;
511
+ return { createJiti, version };
502
512
  }
503
513
 
504
514
  /**
@@ -561,6 +571,7 @@ class ConfigLoader {
561
571
  overrideConfig,
562
572
  hasUnstableNativeNodeJsTSConfigFlag = false,
563
573
  defaultConfigs = [],
574
+ warningService,
564
575
  } = options;
565
576
 
566
577
  debug(
@@ -622,10 +633,7 @@ class ConfigLoader {
622
633
  }
623
634
 
624
635
  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
- );
636
+ warningService.emitEmptyConfigWarning(configFilePath);
629
637
  }
630
638
  }
631
639
 
@@ -713,8 +721,11 @@ class LegacyConfigLoader extends ConfigLoader {
713
721
  * @param {ConfigLoaderOptions} options The options to use when loading configuration files.
714
722
  */
715
723
  constructor(options) {
716
- super(options);
717
- this.#options = options;
724
+ const normalizedOptions = options.warningService
725
+ ? options
726
+ : { ...options, warningService: new WarningService() };
727
+ super(normalizedOptions);
728
+ this.#options = normalizedOptions;
718
729
  }
719
730
 
720
731
  /**
@@ -265,6 +265,27 @@ function getObjectId(object) {
265
265
  return name;
266
266
  }
267
267
 
268
+ /**
269
+ * Asserts that a value is not a function.
270
+ * @param {any} value The value to check.
271
+ * @param {string} key The key of the value in the object.
272
+ * @param {string} objectKey The key of the object being checked.
273
+ * @returns {void}
274
+ * @throws {TypeError} If the value is a function.
275
+ */
276
+ function assertNotFunction(value, key, objectKey) {
277
+ if (typeof value === "function") {
278
+ const error = new TypeError(
279
+ `Cannot serialize key "${key}" in "${objectKey}": Function values are not supported.`,
280
+ );
281
+
282
+ error.messageTemplate = "config-serialize-function";
283
+ error.messageData = { key, objectKey };
284
+
285
+ throw error;
286
+ }
287
+ }
288
+
268
289
  /**
269
290
  * Converts a languageOptions object to a JSON representation.
270
291
  * @param {Record<string, any>} languageOptions The options to create a JSON
@@ -274,6 +295,14 @@ function getObjectId(object) {
274
295
  * @throws {TypeError} If a function is found in the languageOptions.
275
296
  */
276
297
  function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
298
+ if (typeof languageOptions.toJSON === "function") {
299
+ const result = languageOptions.toJSON();
300
+
301
+ assertNotFunction(result, "toJSON", objectKey);
302
+
303
+ return result;
304
+ }
305
+
277
306
  const result = {};
278
307
 
279
308
  for (const [key, value] of Object.entries(languageOptions)) {
@@ -281,7 +310,10 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
281
310
  if (typeof value === "object") {
282
311
  const name = getObjectId(value);
283
312
 
284
- if (name && hasMethod(value)) {
313
+ if (typeof value.toJSON === "function") {
314
+ result[key] = value.toJSON();
315
+ assertNotFunction(result[key], key, objectKey);
316
+ } else if (name && hasMethod(value)) {
285
317
  result[key] = name;
286
318
  } else {
287
319
  result[key] = languageOptionsToJSON(value, key);
@@ -289,16 +321,7 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
289
321
  continue;
290
322
  }
291
323
 
292
- if (typeof value === "function") {
293
- const error = new TypeError(
294
- `Cannot serialize key "${key}" in ${objectKey}: Function values are not supported.`,
295
- );
296
-
297
- error.messageTemplate = "config-serialize-function";
298
- error.messageData = { key, objectKey };
299
-
300
- throw error;
301
- }
324
+ assertNotFunction(value, key, objectKey);
302
325
  }
303
326
 
304
327
  result[key] = value;
@@ -40,6 +40,7 @@ const { pathToFileURL } = require("node:url");
40
40
  const LintResultCache = require("../cli-engine/lint-result-cache");
41
41
  const { Retrier } = require("@humanwhocodes/retry");
42
42
  const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
43
+ const { WarningService } = require("../services/warning-service");
43
44
 
44
45
  /*
45
46
  * This is necessary to allow overwriting writeFile for testing purposes.
@@ -431,10 +432,12 @@ class ESLint {
431
432
  constructor(options = {}) {
432
433
  const defaultConfigs = [];
433
434
  const processedOptions = processOptions(options);
435
+ const warningService = new WarningService();
434
436
  const linter = new Linter({
435
437
  cwd: processedOptions.cwd,
436
438
  configType: "flat",
437
439
  flags: mergeEnvironmentFlags(processedOptions.flags),
440
+ warningService,
438
441
  });
439
442
 
440
443
  const cacheFilePath = getCacheFile(
@@ -457,6 +460,7 @@ class ESLint {
457
460
  hasUnstableNativeNodeJsTSConfigFlag: linter.hasFlag(
458
461
  "unstable_native_nodejs_ts_config",
459
462
  ),
463
+ warningService,
460
464
  };
461
465
 
462
466
  this.#configLoader = linter.hasFlag("unstable_config_lookup_from_file")
@@ -496,10 +500,7 @@ class ESLint {
496
500
 
497
501
  // Check for the .eslintignore file, and warn if it's present.
498
502
  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();
503
504
  }
504
505
  }
505
506
 
@@ -316,6 +316,36 @@ function addDeclaredGlobals(
316
316
 
317
317
  return true;
318
318
  });
319
+
320
+ /*
321
+ * "implicit" contains information about implicit global variables (those created
322
+ * implicitly by assigning values to undeclared variables in non-strict code).
323
+ * Since we augment the global scope using configuration, we need to remove
324
+ * the ones that were added by configuration, as they are either built-in
325
+ * or declared elsewhere, therefore not implicit.
326
+ * Since the "implicit" property was not documented, first we'll check if it exists
327
+ * because it's possible that not all custom scope managers create this property.
328
+ * If it exists, we assume it has properties `variables` and `set`. Property
329
+ * `left` is considered optional (for example, typescript-eslint's scope manage
330
+ * has this property named `leftToBeResolved`).
331
+ */
332
+ const { implicit } = globalScope;
333
+ if (typeof implicit === "object" && implicit !== null) {
334
+ implicit.variables = implicit.variables.filter(variable => {
335
+ const name = variable.name;
336
+ if (globalScope.set.has(name)) {
337
+ implicit.set.delete(name);
338
+ return false;
339
+ }
340
+ return true;
341
+ });
342
+
343
+ if (implicit.left) {
344
+ implicit.left = implicit.left.filter(
345
+ reference => !globalScope.set.has(reference.identifier.name),
346
+ );
347
+ }
348
+ }
319
349
  }
320
350
 
321
351
  /**
@@ -27,7 +27,6 @@ const path = require("node:path"),
27
27
  { SourceCode } = require("../languages/js/source-code"),
28
28
  applyDisableDirectives = require("./apply-disable-directives"),
29
29
  { ConfigCommentParser } = require("@eslint/plugin-kit"),
30
- NodeEventGenerator = require("./node-event-generator"),
31
30
  createReportTranslator = require("./report-translator"),
32
31
  Rules = require("./rules"),
33
32
  createEmitter = require("./safe-emitter"),
@@ -65,8 +64,8 @@ const { FileContext } = require("./file-context");
65
64
  const { ProcessorService } = require("../services/processor-service");
66
65
  const { containsDifferentProperty } = require("../shared/option-utils");
67
66
  const { Config } = require("../config/config");
68
- const STEP_KIND_VISIT = 1;
69
- const STEP_KIND_CALL = 2;
67
+ const { WarningService } = require("../services/warning-service");
68
+ const { SourceCodeTraverser } = require("./source-code-traverser");
70
69
 
71
70
  //------------------------------------------------------------------------------
72
71
  // Typedefs
@@ -113,6 +112,7 @@ const STEP_KIND_CALL = 2;
113
112
  * @property {Map<string, Parser>} parserMap The loaded parsers.
114
113
  * @property {{ passes: TimePass[]; }} times The times spent on applying a rule to a file (see `stats` option).
115
114
  * @property {Rules} ruleMap The loaded rules.
115
+ * @property {WarningService} warningService The warning service.
116
116
  */
117
117
 
118
118
  /**
@@ -1177,9 +1177,6 @@ function runRules(
1177
1177
  ) {
1178
1178
  const emitter = createEmitter();
1179
1179
 
1180
- // must happen first to assign all node.parent properties
1181
- const eventQueue = sourceCode.traverse();
1182
-
1183
1180
  /*
1184
1181
  * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
1185
1182
  * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
@@ -1199,6 +1196,7 @@ function runRules(
1199
1196
  });
1200
1197
 
1201
1198
  const lintingProblems = [];
1199
+ const steps = sourceCode.traverse();
1202
1200
 
1203
1201
  Object.keys(configuredRules).forEach(ruleId => {
1204
1202
  const severity = Config.getRuleNumericSeverity(configuredRules[ruleId]);
@@ -1345,40 +1343,9 @@ function runRules(
1345
1343
  });
1346
1344
  });
1347
1345
 
1348
- const eventGenerator = new NodeEventGenerator(emitter, {
1349
- visitorKeys: sourceCode.visitorKeys ?? language.visitorKeys,
1350
- fallback: Traverser.getKeys,
1351
- matchClass: language.matchesSelectorClass ?? (() => false),
1352
- nodeTypeKey: language.nodeTypeKey,
1353
- });
1354
-
1355
- for (const step of eventQueue) {
1356
- switch (step.kind) {
1357
- case STEP_KIND_VISIT: {
1358
- try {
1359
- if (step.phase === 1) {
1360
- eventGenerator.enterNode(step.target);
1361
- } else {
1362
- eventGenerator.leaveNode(step.target);
1363
- }
1364
- } catch (err) {
1365
- err.currentNode = step.target;
1366
- throw err;
1367
- }
1368
- break;
1369
- }
1370
-
1371
- case STEP_KIND_CALL: {
1372
- emitter.emit(step.target, ...step.args);
1373
- break;
1374
- }
1346
+ const traverser = SourceCodeTraverser.getInstance(language);
1375
1347
 
1376
- default:
1377
- throw new Error(
1378
- `Invalid traversal step found: "${step.type}".`,
1379
- );
1380
- }
1381
- }
1348
+ traverser.traverseSync(sourceCode, emitter, { steps });
1382
1349
 
1383
1350
  return lintingProblems;
1384
1351
  }
@@ -1483,8 +1450,14 @@ class Linter {
1483
1450
  * @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
1484
1451
  * @param {Array<string>} [config.flags] the feature flags to enable.
1485
1452
  * @param {"flat"|"eslintrc"} [config.configType="flat"] the type of config used.
1453
+ * @param {WarningService} [config.warningService] The warning service to use.
1486
1454
  */
1487
- constructor({ cwd, configType = "flat", flags = [] } = {}) {
1455
+ constructor({
1456
+ cwd,
1457
+ configType = "flat",
1458
+ flags = [],
1459
+ warningService = new WarningService(),
1460
+ } = {}) {
1488
1461
  const processedFlags = [];
1489
1462
 
1490
1463
  flags.forEach(flag => {
@@ -1492,11 +1465,10 @@ class Linter {
1492
1465
  const inactiveFlagData = inactiveFlags.get(flag);
1493
1466
  const inactivityReason =
1494
1467
  getInactivityReasonMessage(inactiveFlagData);
1468
+ const message = `The flag '${flag}' is inactive: ${inactivityReason}`;
1495
1469
 
1496
1470
  if (typeof inactiveFlagData.replacedBy === "undefined") {
1497
- throw new Error(
1498
- `The flag '${flag}' is inactive: ${inactivityReason}`,
1499
- );
1471
+ throw new Error(message);
1500
1472
  }
1501
1473
 
1502
1474
  // if there's a replacement, enable it instead of original
@@ -1504,10 +1476,7 @@ class Linter {
1504
1476
  processedFlags.push(inactiveFlagData.replacedBy);
1505
1477
  }
1506
1478
 
1507
- globalThis.process?.emitWarning?.(
1508
- `The flag '${flag}' is inactive: ${inactivityReason}`,
1509
- `ESLintInactiveFlag_${flag}`,
1510
- );
1479
+ warningService.emitInactiveFlagWarning(flag, message);
1511
1480
 
1512
1481
  return;
1513
1482
  }
@@ -1528,6 +1497,7 @@ class Linter {
1528
1497
  configType, // TODO: Remove after flat config conversion
1529
1498
  parserMap: new Map([["espree", espree]]),
1530
1499
  ruleMap: new Rules(),
1500
+ warningService,
1531
1501
  });
1532
1502
 
1533
1503
  this.version = pkg.version;
@@ -2710,6 +2680,14 @@ class Linter {
2710
2680
  options && typeof options.fix !== "undefined" ? options.fix : true;
2711
2681
  const stats = options?.stats;
2712
2682
 
2683
+ const slots = internalSlotsMap.get(this);
2684
+
2685
+ // Remove lint times from the last run.
2686
+ if (stats) {
2687
+ delete slots.times;
2688
+ slots.fixPasses = 0;
2689
+ }
2690
+
2713
2691
  /**
2714
2692
  * This loop continues until one of the following is true:
2715
2693
  *
@@ -2719,14 +2697,6 @@ class Linter {
2719
2697
  * That means anytime a fix is successfully applied, there will be another pass.
2720
2698
  * Essentially, guaranteeing a minimum of two passes.
2721
2699
  */
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
-
2730
2700
  do {
2731
2701
  passNumber++;
2732
2702
  let tTotal;
@@ -2798,9 +2768,8 @@ class Linter {
2798
2768
  debug(
2799
2769
  `Circular fixes detected after pass ${passNumber}. Exiting fix loop.`,
2800
2770
  );
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",
2771
+ slots.warningService.emitCircularFixesWarning(
2772
+ options?.filename ?? "text",
2804
2773
  );
2805
2774
  break;
2806
2775
  }