eslint 9.11.0 → 9.12.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.
@@ -12,7 +12,6 @@
12
12
  const fs = require("node:fs/promises");
13
13
  const { existsSync } = require("node:fs");
14
14
  const path = require("node:path");
15
- const findUp = require("find-up");
16
15
  const { version } = require("../../package.json");
17
16
  const { Linter } = require("../linter");
18
17
  const { getRuleFromConfig } = require("../config/flat-config-helpers");
@@ -39,9 +38,9 @@ const {
39
38
  processOptions
40
39
  } = require("./eslint-helpers");
41
40
  const { pathToFileURL } = require("node:url");
42
- const { FlatConfigArray } = require("../config/flat-config-array");
43
41
  const LintResultCache = require("../cli-engine/lint-result-cache");
44
42
  const { Retrier } = require("@humanwhocodes/retry");
43
+ const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
45
44
 
46
45
  /*
47
46
  * This is necessary to allow overwriting writeFile for testing purposes.
@@ -97,24 +96,8 @@ const { Retrier } = require("@humanwhocodes/retry");
97
96
  // Helpers
98
97
  //------------------------------------------------------------------------------
99
98
 
100
- const FLAT_CONFIG_FILENAMES = [
101
- "eslint.config.js",
102
- "eslint.config.mjs",
103
- "eslint.config.cjs"
104
- ];
105
- const FLAT_CONFIG_FILENAMES_WITH_TS = [
106
- ...FLAT_CONFIG_FILENAMES,
107
- "eslint.config.ts",
108
- "eslint.config.mts",
109
- "eslint.config.cts"
110
- ];
111
99
  const debug = require("debug")("eslint:eslint");
112
100
  const privateMembers = new WeakMap();
113
-
114
- /**
115
- * @type {Map<string, string>}
116
- */
117
- const importedConfigFileModificationTime = new Map();
118
101
  const removedFormatters = new Set([
119
102
  "checkstyle",
120
103
  "codeframe",
@@ -196,12 +179,13 @@ const usedDeprecatedRulesCache = new WeakMap();
196
179
  */
197
180
  function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
198
181
  const {
199
- configs,
200
- options: { cwd }
182
+ options: { cwd },
183
+ configLoader
201
184
  } = privateMembers.get(eslint);
202
185
  const filePath = path.isAbsolute(maybeFilePath)
203
186
  ? maybeFilePath
204
187
  : getPlaceholderPath(cwd);
188
+ const configs = configLoader.getCachedConfigArrayForFile(filePath);
205
189
  const config = configs.getConfig(filePath);
206
190
 
207
191
  // Most files use the same config, so cache it.
@@ -270,269 +254,38 @@ function compareResultsByFilePath(a, b) {
270
254
  return 0;
271
255
  }
272
256
 
273
- /**
274
- * Searches from the current working directory up until finding the
275
- * given flat config filename.
276
- * @param {string} cwd The current working directory to search from.
277
- * @param {boolean} hasUnstableTSConfigFlag `true` if the `unstable_ts_config` flag is enabled, `false` if it's not.
278
- * @returns {Promise<string|undefined>} The filename if found or `undefined` if not.
279
- */
280
- function findFlatConfigFile(cwd, hasUnstableTSConfigFlag) {
281
- const filenames = hasUnstableTSConfigFlag ? FLAT_CONFIG_FILENAMES_WITH_TS : FLAT_CONFIG_FILENAMES;
282
-
283
- return findUp(
284
- filenames,
285
- { cwd }
286
- );
287
- }
288
-
289
- /**
290
- * Check if the file is a TypeScript file.
291
- * @param {string} filePath The file path to check.
292
- * @returns {boolean} `true` if the file is a TypeScript file, `false` if it's not.
293
- */
294
- function isFileTS(filePath) {
295
- const fileExtension = path.extname(filePath);
296
-
297
- return /^\.[mc]?ts$/u.test(fileExtension);
298
- }
299
-
300
- /**
301
- * Check if ESLint is running in Bun.
302
- * @returns {boolean} `true` if the ESLint is running Bun, `false` if it's not.
303
- */
304
- function isRunningInBun() {
305
- return !!globalThis.Bun;
306
- }
307
-
308
- /**
309
- * Check if ESLint is running in Deno.
310
- * @returns {boolean} `true` if the ESLint is running in Deno, `false` if it's not.
311
- */
312
- function isRunningInDeno() {
313
- return !!globalThis.Deno;
314
- }
315
-
316
- /**
317
- * Load the config array from the given filename.
318
- * @param {string} filePath The filename to load from.
319
- * @param {boolean} hasUnstableTSConfigFlag `true` if the `unstable_ts_config` flag is enabled, `false` if it's not.
320
- * @returns {Promise<any>} The config loaded from the config file.
321
- */
322
- async function loadFlatConfigFile(filePath, hasUnstableTSConfigFlag) {
323
- debug(`Loading config from ${filePath}`);
324
-
325
- const fileURL = pathToFileURL(filePath);
326
-
327
- debug(`Config file URL is ${fileURL}`);
328
-
329
- const mtime = (await fs.stat(filePath)).mtime.getTime().toString();
330
-
331
- /*
332
- * Append a query with the config file's modification time (`mtime`) in order
333
- * to import the current version of the config file. Without the query, `import()` would
334
- * cache the config file module by the pathname only, and then always return
335
- * the same version (the one that was actual when the module was imported for the first time).
336
- *
337
- * This ensures that the config file module is loaded and executed again
338
- * if it has been changed since the last time it was imported.
339
- * If it hasn't been changed, `import()` will just return the cached version.
340
- *
341
- * Note that we should not overuse queries (e.g., by appending the current time
342
- * to always reload the config file module) as that could cause memory leaks
343
- * because entries are never removed from the import cache.
344
- */
345
- fileURL.searchParams.append("mtime", mtime);
346
-
347
- /*
348
- * With queries, we can bypass the import cache. However, when import-ing a CJS module,
349
- * Node.js uses the require infrastructure under the hood. That includes the require cache,
350
- * which caches the config file module by its file path (queries have no effect).
351
- * Therefore, we also need to clear the require cache before importing the config file module.
352
- * In order to get the same behavior with ESM and CJS config files, in particular - to reload
353
- * the config file only if it has been changed, we track file modification times and clear
354
- * the require cache only if the file has been changed.
355
- */
356
- if (importedConfigFileModificationTime.get(filePath) !== mtime) {
357
- delete require.cache[filePath];
358
- }
359
-
360
- const isTS = isFileTS(filePath) && hasUnstableTSConfigFlag;
361
-
362
- const isBun = isRunningInBun();
363
-
364
- const isDeno = isRunningInDeno();
365
-
366
- if (isTS && !isDeno && !isBun) {
367
-
368
- const createJiti = await import("jiti").then(jitiModule => jitiModule.default, () => {
369
- throw new Error("The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.");
370
- });
371
-
372
- /*
373
- * Disabling `moduleCache` allows us to reload a
374
- * config file when the last modified timestamp changes.
375
- */
376
-
377
- const jiti = createJiti(__filename, { moduleCache: false });
378
-
379
- if (typeof jiti?.import !== "function") {
380
- throw new Error("You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.");
381
- }
382
-
383
- const config = await jiti.import(fileURL.href);
384
-
385
- importedConfigFileModificationTime.set(filePath, mtime);
386
-
387
- return config?.default ?? config;
388
- }
389
-
390
- const config = (await import(fileURL.href)).default;
391
-
392
- importedConfigFileModificationTime.set(filePath, mtime);
393
-
394
- return config;
395
- }
396
257
 
397
258
  /**
398
259
  * Determines which config file to use. This is determined by seeing if an
399
260
  * override config file was passed, and if so, using it; otherwise, as long
400
261
  * as override config file is not explicitly set to `false`, it will search
401
262
  * upwards from the cwd for a file named `eslint.config.js`.
263
+ *
264
+ * This function is used primarily by the `--inspect-config` option. For now,
265
+ * we will maintain the existing behavior, which is to search up from the cwd.
402
266
  * @param {ESLintOptions} options The ESLint instance options.
403
- * @param {boolean} hasUnstableTSConfigFlag `true` if the `unstable_ts_config` flag is enabled, `false` if it's not.
404
- * @returns {Promise<{configFilePath:string|undefined;basePath:string;error:Error|null}>} Location information for
267
+ * @param {boolean} allowTS `true` if the `unstable_ts_config` flag is enabled, `false` if it's not.
268
+ * @returns {Promise<{configFilePath:string|undefined;basePath:string}>} Location information for
405
269
  * the config file.
406
270
  */
407
- async function locateConfigFileToUse({ configFile, cwd }, hasUnstableTSConfigFlag) {
408
-
409
- // determine where to load config file from
410
- let configFilePath;
411
- let basePath = cwd;
412
- let error = null;
413
-
414
- if (typeof configFile === "string") {
415
- debug(`Override config file path is ${configFile}`);
416
- configFilePath = path.resolve(cwd, configFile);
417
- } else if (configFile !== false) {
418
- debug("Searching for eslint.config.js");
419
- configFilePath = await findFlatConfigFile(cwd, hasUnstableTSConfigFlag);
420
-
421
- if (configFilePath) {
422
- basePath = path.resolve(path.dirname(configFilePath));
423
- } else {
424
- error = new Error("Could not find config file.");
425
- error.messageTemplate = "config-file-missing";
426
- }
271
+ async function locateConfigFileToUse({ configFile, cwd }, allowTS) {
272
+
273
+ const configLoader = new ConfigLoader({
274
+ cwd,
275
+ allowTS,
276
+ configFile
277
+ });
278
+
279
+ const configFilePath = await configLoader.findConfigFileForPath(path.join(cwd, "__placeholder__.js"));
427
280
 
281
+ if (!configFilePath) {
282
+ throw new Error("No ESLint configuration file was found.");
428
283
  }
429
284
 
430
285
  return {
431
286
  configFilePath,
432
- basePath,
433
- error
287
+ basePath: configFile ? cwd : path.dirname(configFilePath)
434
288
  };
435
-
436
- }
437
-
438
- /**
439
- * Calculates the config array for this run based on inputs.
440
- * @param {ESLint} eslint The instance to create the config array for.
441
- * @param {ESLintOptions} options The ESLint instance options.
442
- * @returns {Promise<typeof FlatConfigArray>} The config array for `eslint``.
443
- */
444
- async function calculateConfigArray(eslint, {
445
- cwd,
446
- baseConfig,
447
- overrideConfig,
448
- configFile,
449
- ignore: shouldIgnore,
450
- ignorePatterns
451
- }) {
452
-
453
- // check for cached instance
454
- const slots = privateMembers.get(eslint);
455
-
456
- if (slots.configs) {
457
- return slots.configs;
458
- }
459
-
460
- const hasUnstableTSConfigFlag = eslint.hasFlag("unstable_ts_config");
461
-
462
- const { configFilePath, basePath, error } = await locateConfigFileToUse({ configFile, cwd }, hasUnstableTSConfigFlag);
463
-
464
- // config file is required to calculate config
465
- if (error) {
466
- throw error;
467
- }
468
-
469
- const configs = new FlatConfigArray(baseConfig || [], { basePath, shouldIgnore });
470
-
471
- // load config file
472
- if (configFilePath) {
473
- const fileConfig = await loadFlatConfigFile(configFilePath, hasUnstableTSConfigFlag);
474
-
475
- if (Array.isArray(fileConfig)) {
476
- configs.push(...fileConfig);
477
- } else {
478
- configs.push(fileConfig);
479
- }
480
- }
481
-
482
- // add in any configured defaults
483
- configs.push(...slots.defaultConfigs);
484
-
485
- // append command line ignore patterns
486
- if (ignorePatterns && ignorePatterns.length > 0) {
487
-
488
- let relativeIgnorePatterns;
489
-
490
- /*
491
- * If the config file basePath is different than the cwd, then
492
- * the ignore patterns won't work correctly. Here, we adjust the
493
- * ignore pattern to include the correct relative path. Patterns
494
- * passed as `ignorePatterns` are relative to the cwd, whereas
495
- * the config file basePath can be an ancestor of the cwd.
496
- */
497
- if (basePath === cwd) {
498
- relativeIgnorePatterns = ignorePatterns;
499
- } else {
500
-
501
- // In minimatch patterns, only `/` can be used as path separator
502
- const relativeIgnorePath = path.relative(basePath, cwd).replaceAll(path.sep, "/");
503
-
504
- relativeIgnorePatterns = ignorePatterns.map(pattern => {
505
- const negated = pattern.startsWith("!");
506
- const basePattern = negated ? pattern.slice(1) : pattern;
507
-
508
- return (negated ? "!" : "") +
509
- path.posix.join(relativeIgnorePath, basePattern);
510
- });
511
- }
512
-
513
- /*
514
- * Ignore patterns are added to the end of the config array
515
- * so they can override default ignores.
516
- */
517
- configs.push({
518
- ignores: relativeIgnorePatterns
519
- });
520
- }
521
-
522
- if (overrideConfig) {
523
- if (Array.isArray(overrideConfig)) {
524
- configs.push(...overrideConfig);
525
- } else {
526
- configs.push(overrideConfig);
527
- }
528
- }
529
-
530
- await configs.normalize();
531
-
532
- // cache the config array for this instance
533
- slots.configs = configs;
534
-
535
- return configs;
536
289
  }
537
290
 
538
291
  /**
@@ -678,6 +431,12 @@ class ESLint {
678
431
  */
679
432
  static configType = "flat";
680
433
 
434
+ /**
435
+ * The loader to use for finding config files.
436
+ * @type {ConfigLoader|LegacyConfigLoader}
437
+ */
438
+ #configLoader;
439
+
681
440
  /**
682
441
  * Creates a new instance of the main ESLint API.
683
442
  * @param {ESLintOptions} options The options for this instance.
@@ -701,15 +460,34 @@ class ESLint {
701
460
  ? new LintResultCache(cacheFilePath, processedOptions.cacheStrategy)
702
461
  : null;
703
462
 
463
+ const configLoaderOptions = {
464
+ cwd: processedOptions.cwd,
465
+ baseConfig: processedOptions.baseConfig,
466
+ overrideConfig: processedOptions.overrideConfig,
467
+ configFile: processedOptions.configFile,
468
+ ignoreEnabled: processedOptions.ignore,
469
+ ignorePatterns: processedOptions.ignorePatterns,
470
+ defaultConfigs,
471
+ allowTS: processedOptions.flags.includes("unstable_ts_config")
472
+ };
473
+
474
+ this.#configLoader = processedOptions.flags.includes("unstable_config_lookup_from_file")
475
+ ? new ConfigLoader(configLoaderOptions)
476
+ : new LegacyConfigLoader(configLoaderOptions);
477
+
478
+ debug(`Using config loader ${this.#configLoader.constructor.name}`);
479
+
704
480
  privateMembers.set(this, {
705
481
  options: processedOptions,
706
482
  linter,
707
483
  cacheFilePath,
708
484
  lintResultCache,
709
485
  defaultConfigs,
710
- configs: null
486
+ configs: null,
487
+ configLoader: this.#configLoader
711
488
  });
712
489
 
490
+
713
491
  /**
714
492
  * If additional plugins are passed in, add that to the default
715
493
  * configs for this instance.
@@ -813,20 +591,10 @@ class ESLint {
813
591
 
814
592
  const resultRules = new Map();
815
593
  const {
816
- configs,
594
+ configLoader,
817
595
  options: { cwd }
818
596
  } = privateMembers.get(this);
819
597
 
820
- /*
821
- * We can only accurately return rules meta information for linting results if the
822
- * results were created by this instance. Otherwise, the necessary rules data is
823
- * not available. So if the config array doesn't already exist, just throw an error
824
- * to let the user know we can't do anything here.
825
- */
826
- if (!configs) {
827
- throw createExtraneousResultsError();
828
- }
829
-
830
598
  for (const result of results) {
831
599
 
832
600
  /*
@@ -845,6 +613,14 @@ class ESLint {
845
613
  * All of the plugin and rule information is contained within the
846
614
  * calculated config for the given file.
847
615
  */
616
+ let configs;
617
+
618
+ try {
619
+ configs = configLoader.getCachedConfigArrayForFile(filePath);
620
+ } catch {
621
+ throw createExtraneousResultsError();
622
+ }
623
+
848
624
  const config = configs.getConfig(filePath);
849
625
 
850
626
  if (!config) {
@@ -919,7 +695,6 @@ class ESLint {
919
695
 
920
696
  debug(`Using file patterns: ${normalizedPatterns}`);
921
697
 
922
- const configs = await calculateConfigArray(this, eslintOptions);
923
698
  const {
924
699
  allowInlineConfig,
925
700
  cache,
@@ -955,7 +730,7 @@ class ESLint {
955
730
  patterns: normalizedPatterns,
956
731
  cwd,
957
732
  globInputPaths,
958
- configs,
733
+ configLoader: this.#configLoader,
959
734
  errorOnUnmatchedPattern
960
735
  });
961
736
  const controller = new AbortController();
@@ -972,8 +747,9 @@ class ESLint {
972
747
  */
973
748
  const results = await Promise.all(
974
749
 
975
- filePaths.map(filePath => {
750
+ filePaths.map(async filePath => {
976
751
 
752
+ const configs = await this.#configLoader.loadConfigArrayForFile(filePath);
977
753
  const config = configs.getConfig(filePath);
978
754
 
979
755
  /*
@@ -1111,7 +887,6 @@ class ESLint {
1111
887
  linter,
1112
888
  options: eslintOptions
1113
889
  } = privateMembers.get(this);
1114
- const configs = await calculateConfigArray(this, eslintOptions);
1115
890
  const {
1116
891
  allowInlineConfig,
1117
892
  cwd,
@@ -1125,21 +900,21 @@ class ESLint {
1125
900
  const startTime = Date.now();
1126
901
  const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
1127
902
  const resolvedFilename = path.resolve(cwd, filePath || "__placeholder__.js");
1128
- const config = configs.getConfig(resolvedFilename);
1129
-
1130
- const fixer = getFixerForFixTypes(fix, fixTypesSet, config);
903
+ const configs = await this.#configLoader.loadConfigArrayForFile(resolvedFilename);
904
+ const configStatus = configs?.getConfigStatus(resolvedFilename) ?? "unconfigured";
1131
905
 
1132
906
  // Clear the last used config arrays.
1133
- if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) {
907
+ if (resolvedFilename && configStatus !== "matched") {
1134
908
  const shouldWarnIgnored = typeof warnIgnored === "boolean" ? warnIgnored : constructorWarnIgnored;
1135
909
 
1136
910
  if (shouldWarnIgnored) {
1137
- const configStatus = configs.getConfigStatus(resolvedFilename);
1138
-
1139
911
  results.push(createIgnoreResult(resolvedFilename, cwd, configStatus));
1140
912
  }
1141
913
  } else {
1142
914
 
915
+ const config = configs.getConfig(resolvedFilename);
916
+ const fixer = getFixerForFixTypes(fix, fixTypesSet, config);
917
+
1143
918
  // Do lint.
1144
919
  results.push(verifyText({
1145
920
  text: code,
@@ -1271,7 +1046,14 @@ class ESLint {
1271
1046
  }
1272
1047
  const options = privateMembers.get(this).options;
1273
1048
  const absolutePath = path.resolve(options.cwd, filePath);
1274
- const configs = await calculateConfigArray(this, options);
1049
+ const configs = await this.#configLoader.loadConfigArrayForFile(absolutePath);
1050
+
1051
+ if (!configs) {
1052
+ const error = new Error("Could not find config file.");
1053
+
1054
+ error.messageTemplate = "config-file-missing";
1055
+ throw error;
1056
+ }
1275
1057
 
1276
1058
  return configs.getConfig(absolutePath);
1277
1059
  }
@@ -1279,14 +1061,22 @@ class ESLint {
1279
1061
  /**
1280
1062
  * Finds the config file being used by this instance based on the options
1281
1063
  * passed to the constructor.
1064
+ * @param {string} [filePath] The path of the file to find the config file for.
1282
1065
  * @returns {Promise<string|undefined>} The path to the config file being used or
1283
1066
  * `undefined` if no config file is being used.
1284
1067
  */
1285
- async findConfigFile() {
1068
+ findConfigFile(filePath) {
1286
1069
  const options = privateMembers.get(this).options;
1287
- const { configFilePath } = await locateConfigFileToUse(options, this.hasFlag("unstable_ts_config"));
1288
1070
 
1289
- return configFilePath;
1071
+ /*
1072
+ * Because the new config lookup scheme skips the current directory
1073
+ * and looks into the parent directories, we need to use a placeholder
1074
+ * directory to ensure the file in cwd is checked.
1075
+ */
1076
+ const fakeCwd = path.join(options.cwd, "__placeholder__");
1077
+
1078
+ return this.#configLoader.findConfigFileForPath(filePath ?? fakeCwd)
1079
+ .catch(() => void 0);
1290
1080
  }
1291
1081
 
1292
1082
  /**
@@ -47,6 +47,8 @@ const { SourceCode } = require("../languages/js/source-code");
47
47
  * @property {string} [name] Name for the test case.
48
48
  * @property {string} code Code for the test case.
49
49
  * @property {any[]} [options] Options for the test case.
50
+ * @property {Function} [before] Function to execute before testing the case.
51
+ * @property {Function} [after] Function to execute after testing the case regardless of its result.
50
52
  * @property {LanguageOptions} [languageOptions] The language options to use in the test case.
51
53
  * @property {{ [name: string]: any }} [settings] Settings for the test case.
52
54
  * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
@@ -61,6 +63,8 @@ const { SourceCode } = require("../languages/js/source-code");
61
63
  * @property {number | Array<TestCaseError | string | RegExp>} errors Expected errors.
62
64
  * @property {string | null} [output] The expected code after autofixes are applied. If set to `null`, the test runner will assert that no autofix is suggested.
63
65
  * @property {any[]} [options] Options for the test case.
66
+ * @property {Function} [before] Function to execute before testing the case.
67
+ * @property {Function} [after] Function to execute after testing the case regardless of its result.
64
68
  * @property {{ [name: string]: any }} [settings] Settings for the test case.
65
69
  * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
66
70
  * @property {LanguageOptions} [languageOptions] The language options to use in the test case.
@@ -105,6 +109,8 @@ const RuleTesterParameters = [
105
109
  "code",
106
110
  "filename",
107
111
  "options",
112
+ "before",
113
+ "after",
108
114
  "errors",
109
115
  "output",
110
116
  "only"
@@ -621,6 +627,21 @@ class RuleTester {
621
627
  ...defaultConfig.slice(1)
622
628
  ];
623
629
 
630
+ /**
631
+ * Runs a hook on the given item when it's assigned to the given property
632
+ * @param {string|Object} item Item to run the hook on
633
+ * @param {string} prop The property having the hook assigned to
634
+ * @throws {Error} If the property is not a function or that function throws an error
635
+ * @returns {void}
636
+ * @private
637
+ */
638
+ function runHook(item, prop) {
639
+ if (typeof item === "object" && hasOwnProperty(item, prop)) {
640
+ assert.strictEqual(typeof item[prop], "function", `Optional test case property '${prop}' must be a function`);
641
+ item[prop]();
642
+ }
643
+ }
644
+
624
645
  /**
625
646
  * Run the rule for the given item
626
647
  * @param {string|Object} item Item to run the rule against
@@ -1258,7 +1279,12 @@ class RuleTester {
1258
1279
  this.constructor[valid.only ? "itOnly" : "it"](
1259
1280
  sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
1260
1281
  () => {
1261
- testValidTemplate(valid);
1282
+ try {
1283
+ runHook(valid, "before");
1284
+ testValidTemplate(valid);
1285
+ } finally {
1286
+ runHook(valid, "after");
1287
+ }
1262
1288
  }
1263
1289
  );
1264
1290
  });
@@ -1271,7 +1297,12 @@ class RuleTester {
1271
1297
  this.constructor[invalid.only ? "itOnly" : "it"](
1272
1298
  sanitize(invalid.name || invalid.code),
1273
1299
  () => {
1274
- testInvalidTemplate(invalid);
1300
+ try {
1301
+ runHook(invalid, "before");
1302
+ testInvalidTemplate(invalid);
1303
+ } finally {
1304
+ runHook(invalid, "after");
1305
+ }
1275
1306
  }
1276
1307
  );
1277
1308
  });
@@ -45,6 +45,9 @@ module.exports = {
45
45
  max: {
46
46
  type: "integer",
47
47
  minimum: 0
48
+ },
49
+ variant: {
50
+ enum: ["classic", "modified"]
48
51
  }
49
52
  },
50
53
  additionalProperties: false
@@ -61,16 +64,22 @@ module.exports = {
61
64
  create(context) {
62
65
  const option = context.options[0];
63
66
  let THRESHOLD = 20;
67
+ let VARIANT = "classic";
68
+
69
+ if (typeof option === "object") {
70
+ if (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max")) {
71
+ THRESHOLD = option.maximum || option.max;
72
+ }
64
73
 
65
- if (
66
- typeof option === "object" &&
67
- (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max"))
68
- ) {
69
- THRESHOLD = option.maximum || option.max;
74
+ if (Object.hasOwn(option, "variant")) {
75
+ VARIANT = option.variant;
76
+ }
70
77
  } else if (typeof option === "number") {
71
78
  THRESHOLD = option;
72
79
  }
73
80
 
81
+ const IS_MODIFIED_COMPLEXITY = VARIANT === "modified";
82
+
74
83
  //--------------------------------------------------------------------------
75
84
  // Helpers
76
85
  //--------------------------------------------------------------------------
@@ -112,7 +121,8 @@ module.exports = {
112
121
  AssignmentPattern: increaseComplexity,
113
122
 
114
123
  // Avoid `default`
115
- "SwitchCase[test]": increaseComplexity,
124
+ "SwitchCase[test]": () => IS_MODIFIED_COMPLEXITY || increaseComplexity(),
125
+ SwitchStatement: () => IS_MODIFIED_COMPLEXITY && increaseComplexity(),
116
126
 
117
127
  // Logical assignment operators have short-circuiting behavior
118
128
  AssignmentExpression(node) {
@@ -10,6 +10,7 @@
10
10
  */
11
11
  const activeFlags = new Map([
12
12
  ["test_only", "Used only for testing."],
13
+ ["unstable_config_lookup_from_file", "Look up eslint.config.js from the file being linted."],
13
14
  ["unstable_ts_config", "Enable TypeScript configuration files."]
14
15
  ]);
15
16