]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/cli-engine/config-array-factory.js
first commit
[pve-eslint.git] / eslint / lib / cli-engine / config-array-factory.js
1 /**
2 * @fileoverview The factory of `ConfigArray` objects.
3 *
4 * This class provides methods to create `ConfigArray` instance.
5 *
6 * - `create(configData, options)`
7 * Create a `ConfigArray` instance from a config data. This is to handle CLI
8 * options except `--config`.
9 * - `loadFile(filePath, options)`
10 * Create a `ConfigArray` instance from a config file. This is to handle
11 * `--config` option. If the file was not found, throws the following error:
12 * - If the filename was `*.js`, a `MODULE_NOT_FOUND` error.
13 * - If the filename was `package.json`, an IO error or an
14 * `ESLINT_CONFIG_FIELD_NOT_FOUND` error.
15 * - Otherwise, an IO error such as `ENOENT`.
16 * - `loadInDirectory(directoryPath, options)`
17 * Create a `ConfigArray` instance from a config file which is on a given
18 * directory. This tries to load `.eslintrc.*` or `package.json`. If not
19 * found, returns an empty `ConfigArray`.
20 * - `loadESLintIgnore(filePath)`
21 * Create a `ConfigArray` instance from a config file that is `.eslintignore`
22 * format. This is to handle `--ignore-path` option.
23 * - `loadDefaultESLintIgnore()`
24 * Create a `ConfigArray` instance from `.eslintignore` or `package.json` in
25 * the current working directory.
26 *
27 * `ConfigArrayFactory` class has the responsibility that loads configuration
28 * files, including loading `extends`, `parser`, and `plugins`. The created
29 * `ConfigArray` instance has the loaded `extends`, `parser`, and `plugins`.
30 *
31 * But this class doesn't handle cascading. `CascadingConfigArrayFactory` class
32 * handles cascading and hierarchy.
33 *
34 * @author Toru Nagashima <https://github.com/mysticatea>
35 */
36 "use strict";
37
38 //------------------------------------------------------------------------------
39 // Requirements
40 //------------------------------------------------------------------------------
41
42 const fs = require("fs");
43 const path = require("path");
44 const importFresh = require("import-fresh");
45 const stripComments = require("strip-json-comments");
46 const { validateConfigSchema } = require("../shared/config-validator");
47 const naming = require("../shared/naming");
48 const ModuleResolver = require("../shared/relative-module-resolver");
49 const {
50 ConfigArray,
51 ConfigDependency,
52 IgnorePattern,
53 OverrideTester
54 } = require("./config-array");
55 const debug = require("debug")("eslint:config-array-factory");
56
57 //------------------------------------------------------------------------------
58 // Helpers
59 //------------------------------------------------------------------------------
60
61 const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
62 const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js");
63 const configFilenames = [
64 ".eslintrc.js",
65 ".eslintrc.cjs",
66 ".eslintrc.yaml",
67 ".eslintrc.yml",
68 ".eslintrc.json",
69 ".eslintrc",
70 "package.json"
71 ];
72
73 // Define types for VSCode IntelliSense.
74 /** @typedef {import("../shared/types").ConfigData} ConfigData */
75 /** @typedef {import("../shared/types").OverrideConfigData} OverrideConfigData */
76 /** @typedef {import("../shared/types").Parser} Parser */
77 /** @typedef {import("../shared/types").Plugin} Plugin */
78 /** @typedef {import("./config-array/config-dependency").DependentParser} DependentParser */
79 /** @typedef {import("./config-array/config-dependency").DependentPlugin} DependentPlugin */
80 /** @typedef {ConfigArray[0]} ConfigArrayElement */
81
82 /**
83 * @typedef {Object} ConfigArrayFactoryOptions
84 * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
85 * @property {string} [cwd] The path to the current working directory.
86 * @property {string} [resolvePluginsRelativeTo] A path to the directory that plugins should be resolved from. Defaults to `cwd`.
87 */
88
89 /**
90 * @typedef {Object} ConfigArrayFactoryInternalSlots
91 * @property {Map<string,Plugin>} additionalPluginPool The map for additional plugins.
92 * @property {string} cwd The path to the current working directory.
93 * @property {string | undefined} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
94 */
95
96 /**
97 * @typedef {Object} ConfigArrayFactoryLoadingContext
98 * @property {string} filePath The path to the current configuration.
99 * @property {string} matchBasePath The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
100 * @property {string} name The name of the current configuration.
101 * @property {string} pluginBasePath The base path to resolve plugins.
102 * @property {"config" | "ignore" | "implicit-processor"} type The type of the current configuration. This is `"config"` in normal. This is `"ignore"` if it came from `.eslintignore`. This is `"implicit-processor"` if it came from legacy file-extension processors.
103 */
104
105 /**
106 * @typedef {Object} ConfigArrayFactoryLoadingContext
107 * @property {string} filePath The path to the current configuration.
108 * @property {string} matchBasePath The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
109 * @property {string} name The name of the current configuration.
110 * @property {"config" | "ignore" | "implicit-processor"} type The type of the current configuration. This is `"config"` in normal. This is `"ignore"` if it came from `.eslintignore`. This is `"implicit-processor"` if it came from legacy file-extension processors.
111 */
112
113 /** @type {WeakMap<ConfigArrayFactory, ConfigArrayFactoryInternalSlots>} */
114 const internalSlotsMap = new WeakMap();
115
116 /**
117 * Check if a given string is a file path.
118 * @param {string} nameOrPath A module name or file path.
119 * @returns {boolean} `true` if the `nameOrPath` is a file path.
120 */
121 function isFilePath(nameOrPath) {
122 return (
123 /^\.{1,2}[/\\]/u.test(nameOrPath) ||
124 path.isAbsolute(nameOrPath)
125 );
126 }
127
128 /**
129 * Convenience wrapper for synchronously reading file contents.
130 * @param {string} filePath The filename to read.
131 * @returns {string} The file contents, with the BOM removed.
132 * @private
133 */
134 function readFile(filePath) {
135 return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/u, "");
136 }
137
138 /**
139 * Loads a YAML configuration from a file.
140 * @param {string} filePath The filename to load.
141 * @returns {ConfigData} The configuration object from the file.
142 * @throws {Error} If the file cannot be read.
143 * @private
144 */
145 function loadYAMLConfigFile(filePath) {
146 debug(`Loading YAML config file: ${filePath}`);
147
148 // lazy load YAML to improve performance when not used
149 const yaml = require("js-yaml");
150
151 try {
152
153 // empty YAML file can be null, so always use
154 return yaml.safeLoad(readFile(filePath)) || {};
155 } catch (e) {
156 debug(`Error reading YAML file: ${filePath}`);
157 e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
158 throw e;
159 }
160 }
161
162 /**
163 * Loads a JSON configuration from a file.
164 * @param {string} filePath The filename to load.
165 * @returns {ConfigData} The configuration object from the file.
166 * @throws {Error} If the file cannot be read.
167 * @private
168 */
169 function loadJSONConfigFile(filePath) {
170 debug(`Loading JSON config file: ${filePath}`);
171
172 try {
173 return JSON.parse(stripComments(readFile(filePath)));
174 } catch (e) {
175 debug(`Error reading JSON file: ${filePath}`);
176 e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
177 e.messageTemplate = "failed-to-read-json";
178 e.messageData = {
179 path: filePath,
180 message: e.message
181 };
182 throw e;
183 }
184 }
185
186 /**
187 * Loads a legacy (.eslintrc) configuration from a file.
188 * @param {string} filePath The filename to load.
189 * @returns {ConfigData} The configuration object from the file.
190 * @throws {Error} If the file cannot be read.
191 * @private
192 */
193 function loadLegacyConfigFile(filePath) {
194 debug(`Loading legacy config file: ${filePath}`);
195
196 // lazy load YAML to improve performance when not used
197 const yaml = require("js-yaml");
198
199 try {
200 return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
201 } catch (e) {
202 debug("Error reading YAML file: %s\n%o", filePath, e);
203 e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
204 throw e;
205 }
206 }
207
208 /**
209 * Loads a JavaScript configuration from a file.
210 * @param {string} filePath The filename to load.
211 * @returns {ConfigData} The configuration object from the file.
212 * @throws {Error} If the file cannot be read.
213 * @private
214 */
215 function loadJSConfigFile(filePath) {
216 debug(`Loading JS config file: ${filePath}`);
217 try {
218 return importFresh(filePath);
219 } catch (e) {
220 debug(`Error reading JavaScript file: ${filePath}`);
221 e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
222 throw e;
223 }
224 }
225
226 /**
227 * Loads a configuration from a package.json file.
228 * @param {string} filePath The filename to load.
229 * @returns {ConfigData} The configuration object from the file.
230 * @throws {Error} If the file cannot be read.
231 * @private
232 */
233 function loadPackageJSONConfigFile(filePath) {
234 debug(`Loading package.json config file: ${filePath}`);
235 try {
236 const packageData = loadJSONConfigFile(filePath);
237
238 if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) {
239 throw Object.assign(
240 new Error("package.json file doesn't have 'eslintConfig' field."),
241 { code: "ESLINT_CONFIG_FIELD_NOT_FOUND" }
242 );
243 }
244
245 return packageData.eslintConfig;
246 } catch (e) {
247 debug(`Error reading package.json file: ${filePath}`);
248 e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
249 throw e;
250 }
251 }
252
253 /**
254 * Loads a `.eslintignore` from a file.
255 * @param {string} filePath The filename to load.
256 * @returns {string[]} The ignore patterns from the file.
257 * @private
258 */
259 function loadESLintIgnoreFile(filePath) {
260 debug(`Loading .eslintignore file: ${filePath}`);
261
262 try {
263 return readFile(filePath)
264 .split(/\r?\n/gu)
265 .filter(line => line.trim() !== "" && !line.startsWith("#"));
266 } catch (e) {
267 debug(`Error reading .eslintignore file: ${filePath}`);
268 e.message = `Cannot read .eslintignore file: ${filePath}\nError: ${e.message}`;
269 throw e;
270 }
271 }
272
273 /**
274 * Creates an error to notify about a missing config to extend from.
275 * @param {string} configName The name of the missing config.
276 * @param {string} importerName The name of the config that imported the missing config
277 * @returns {Error} The error object to throw
278 * @private
279 */
280 function configMissingError(configName, importerName) {
281 return Object.assign(
282 new Error(`Failed to load config "${configName}" to extend from.`),
283 {
284 messageTemplate: "extend-config-missing",
285 messageData: { configName, importerName }
286 }
287 );
288 }
289
290 /**
291 * Loads a configuration file regardless of the source. Inspects the file path
292 * to determine the correctly way to load the config file.
293 * @param {string} filePath The path to the configuration.
294 * @returns {ConfigData|null} The configuration information.
295 * @private
296 */
297 function loadConfigFile(filePath) {
298 switch (path.extname(filePath)) {
299 case ".js":
300 case ".cjs":
301 return loadJSConfigFile(filePath);
302
303 case ".json":
304 if (path.basename(filePath) === "package.json") {
305 return loadPackageJSONConfigFile(filePath);
306 }
307 return loadJSONConfigFile(filePath);
308
309 case ".yaml":
310 case ".yml":
311 return loadYAMLConfigFile(filePath);
312
313 default:
314 return loadLegacyConfigFile(filePath);
315 }
316 }
317
318 /**
319 * Write debug log.
320 * @param {string} request The requested module name.
321 * @param {string} relativeTo The file path to resolve the request relative to.
322 * @param {string} filePath The resolved file path.
323 * @returns {void}
324 */
325 function writeDebugLogForLoading(request, relativeTo, filePath) {
326 /* istanbul ignore next */
327 if (debug.enabled) {
328 let nameAndVersion = null;
329
330 try {
331 const packageJsonPath = ModuleResolver.resolve(
332 `${request}/package.json`,
333 relativeTo
334 );
335 const { version = "unknown" } = require(packageJsonPath);
336
337 nameAndVersion = `${request}@${version}`;
338 } catch (error) {
339 debug("package.json was not found:", error.message);
340 nameAndVersion = request;
341 }
342
343 debug("Loaded: %s (%s)", nameAndVersion, filePath);
344 }
345 }
346
347 /**
348 * Create a new context with default values.
349 * @param {ConfigArrayFactoryInternalSlots} slots The internal slots.
350 * @param {"config" | "ignore" | "implicit-processor" | undefined} providedType The type of the current configuration. Default is `"config"`.
351 * @param {string | undefined} providedName The name of the current configuration. Default is the relative path from `cwd` to `filePath`.
352 * @param {string | undefined} providedFilePath The path to the current configuration. Default is empty string.
353 * @param {string | undefined} providedMatchBasePath The type of the current configuration. Default is the directory of `filePath` or `cwd`.
354 * @returns {ConfigArrayFactoryLoadingContext} The created context.
355 */
356 function createContext(
357 { cwd, resolvePluginsRelativeTo },
358 providedType,
359 providedName,
360 providedFilePath,
361 providedMatchBasePath
362 ) {
363 const filePath = providedFilePath
364 ? path.resolve(cwd, providedFilePath)
365 : "";
366 const matchBasePath =
367 (providedMatchBasePath && path.resolve(cwd, providedMatchBasePath)) ||
368 (filePath && path.dirname(filePath)) ||
369 cwd;
370 const name =
371 providedName ||
372 (filePath && path.relative(cwd, filePath)) ||
373 "";
374 const pluginBasePath =
375 resolvePluginsRelativeTo ||
376 (filePath && path.dirname(filePath)) ||
377 cwd;
378 const type = providedType || "config";
379
380 return { filePath, matchBasePath, name, pluginBasePath, type };
381 }
382
383 /**
384 * Normalize a given plugin.
385 * - Ensure the object to have four properties: configs, environments, processors, and rules.
386 * - Ensure the object to not have other properties.
387 * @param {Plugin} plugin The plugin to normalize.
388 * @returns {Plugin} The normalized plugin.
389 */
390 function normalizePlugin(plugin) {
391 return {
392 configs: plugin.configs || {},
393 environments: plugin.environments || {},
394 processors: plugin.processors || {},
395 rules: plugin.rules || {}
396 };
397 }
398
399 //------------------------------------------------------------------------------
400 // Public Interface
401 //------------------------------------------------------------------------------
402
403 /**
404 * The factory of `ConfigArray` objects.
405 */
406 class ConfigArrayFactory {
407
408 /**
409 * Initialize this instance.
410 * @param {ConfigArrayFactoryOptions} [options] The map for additional plugins.
411 */
412 constructor({
413 additionalPluginPool = new Map(),
414 cwd = process.cwd(),
415 resolvePluginsRelativeTo
416 } = {}) {
417 internalSlotsMap.set(this, {
418 additionalPluginPool,
419 cwd,
420 resolvePluginsRelativeTo:
421 resolvePluginsRelativeTo &&
422 path.resolve(cwd, resolvePluginsRelativeTo)
423 });
424 }
425
426 /**
427 * Create `ConfigArray` instance from a config data.
428 * @param {ConfigData|null} configData The config data to create.
429 * @param {Object} [options] The options.
430 * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
431 * @param {string} [options.filePath] The path to this config data.
432 * @param {string} [options.name] The config name.
433 * @returns {ConfigArray} Loaded config.
434 */
435 create(configData, { basePath, filePath, name } = {}) {
436 if (!configData) {
437 return new ConfigArray();
438 }
439
440 const slots = internalSlotsMap.get(this);
441 const ctx = createContext(slots, "config", name, filePath, basePath);
442 const elements = this._normalizeConfigData(configData, ctx);
443
444 return new ConfigArray(...elements);
445 }
446
447 /**
448 * Load a config file.
449 * @param {string} filePath The path to a config file.
450 * @param {Object} [options] The options.
451 * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
452 * @param {string} [options.name] The config name.
453 * @returns {ConfigArray} Loaded config.
454 */
455 loadFile(filePath, { basePath, name } = {}) {
456 const slots = internalSlotsMap.get(this);
457 const ctx = createContext(slots, "config", name, filePath, basePath);
458
459 return new ConfigArray(...this._loadConfigData(ctx));
460 }
461
462 /**
463 * Load the config file on a given directory if exists.
464 * @param {string} directoryPath The path to a directory.
465 * @param {Object} [options] The options.
466 * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
467 * @param {string} [options.name] The config name.
468 * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
469 */
470 loadInDirectory(directoryPath, { basePath, name } = {}) {
471 const slots = internalSlotsMap.get(this);
472
473 for (const filename of configFilenames) {
474 const ctx = createContext(
475 slots,
476 "config",
477 name,
478 path.join(directoryPath, filename),
479 basePath
480 );
481
482 if (fs.existsSync(ctx.filePath)) {
483 let configData;
484
485 try {
486 configData = loadConfigFile(ctx.filePath);
487 } catch (error) {
488 if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
489 throw error;
490 }
491 }
492
493 if (configData) {
494 debug(`Config file found: ${ctx.filePath}`);
495 return new ConfigArray(
496 ...this._normalizeConfigData(configData, ctx)
497 );
498 }
499 }
500 }
501
502 debug(`Config file not found on ${directoryPath}`);
503 return new ConfigArray();
504 }
505
506 /**
507 * Check if a config file on a given directory exists or not.
508 * @param {string} directoryPath The path to a directory.
509 * @returns {string | null} The path to the found config file. If not found then null.
510 */
511 static getPathToConfigFileInDirectory(directoryPath) {
512 for (const filename of configFilenames) {
513 const filePath = path.join(directoryPath, filename);
514
515 if (fs.existsSync(filePath)) {
516 if (filename === "package.json") {
517 try {
518 loadPackageJSONConfigFile(filePath);
519 return filePath;
520 } catch (error) { /* ignore */ }
521 } else {
522 return filePath;
523 }
524 }
525 }
526 return null;
527 }
528
529 /**
530 * Load `.eslintignore` file.
531 * @param {string} filePath The path to a `.eslintignore` file to load.
532 * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
533 */
534 loadESLintIgnore(filePath) {
535 const slots = internalSlotsMap.get(this);
536 const ctx = createContext(
537 slots,
538 "ignore",
539 void 0,
540 filePath,
541 slots.cwd
542 );
543 const ignorePatterns = loadESLintIgnoreFile(ctx.filePath);
544
545 return new ConfigArray(
546 ...this._normalizeESLintIgnoreData(ignorePatterns, ctx)
547 );
548 }
549
550 /**
551 * Load `.eslintignore` file in the current working directory.
552 * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
553 */
554 loadDefaultESLintIgnore() {
555 const slots = internalSlotsMap.get(this);
556 const eslintIgnorePath = path.resolve(slots.cwd, ".eslintignore");
557 const packageJsonPath = path.resolve(slots.cwd, "package.json");
558
559 if (fs.existsSync(eslintIgnorePath)) {
560 return this.loadESLintIgnore(eslintIgnorePath);
561 }
562 if (fs.existsSync(packageJsonPath)) {
563 const data = loadJSONConfigFile(packageJsonPath);
564
565 if (Object.hasOwnProperty.call(data, "eslintIgnore")) {
566 if (!Array.isArray(data.eslintIgnore)) {
567 throw new Error("Package.json eslintIgnore property requires an array of paths");
568 }
569 const ctx = createContext(
570 slots,
571 "ignore",
572 "eslintIgnore in package.json",
573 packageJsonPath,
574 slots.cwd
575 );
576
577 return new ConfigArray(
578 ...this._normalizeESLintIgnoreData(data.eslintIgnore, ctx)
579 );
580 }
581 }
582
583 return new ConfigArray();
584 }
585
586 /**
587 * Load a given config file.
588 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
589 * @returns {IterableIterator<ConfigArrayElement>} Loaded config.
590 * @private
591 */
592 _loadConfigData(ctx) {
593 return this._normalizeConfigData(loadConfigFile(ctx.filePath), ctx);
594 }
595
596 /**
597 * Normalize a given `.eslintignore` data to config array elements.
598 * @param {string[]} ignorePatterns The patterns to ignore files.
599 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
600 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
601 * @private
602 */
603 *_normalizeESLintIgnoreData(ignorePatterns, ctx) {
604 const elements = this._normalizeObjectConfigData(
605 { ignorePatterns },
606 ctx
607 );
608
609 // Set `ignorePattern.loose` flag for backward compatibility.
610 for (const element of elements) {
611 if (element.ignorePattern) {
612 element.ignorePattern.loose = true;
613 }
614 yield element;
615 }
616 }
617
618 /**
619 * Normalize a given config to an array.
620 * @param {ConfigData} configData The config data to normalize.
621 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
622 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
623 * @private
624 */
625 _normalizeConfigData(configData, ctx) {
626 validateConfigSchema(configData, ctx.name || ctx.filePath);
627 return this._normalizeObjectConfigData(configData, ctx);
628 }
629
630 /**
631 * Normalize a given config to an array.
632 * @param {ConfigData|OverrideConfigData} configData The config data to normalize.
633 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
634 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
635 * @private
636 */
637 *_normalizeObjectConfigData(configData, ctx) {
638 const { files, excludedFiles, ...configBody } = configData;
639 const criteria = OverrideTester.create(
640 files,
641 excludedFiles,
642 ctx.matchBasePath
643 );
644 const elements = this._normalizeObjectConfigDataBody(configBody, ctx);
645
646 // Apply the criteria to every element.
647 for (const element of elements) {
648
649 /*
650 * Merge the criteria.
651 * This is for the `overrides` entries that came from the
652 * configurations of `overrides[].extends`.
653 */
654 element.criteria = OverrideTester.and(criteria, element.criteria);
655
656 /*
657 * Remove `root` property to ignore `root` settings which came from
658 * `extends` in `overrides`.
659 */
660 if (element.criteria) {
661 element.root = void 0;
662 }
663
664 yield element;
665 }
666 }
667
668 /**
669 * Normalize a given config to an array.
670 * @param {ConfigData} configData The config data to normalize.
671 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
672 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
673 * @private
674 */
675 *_normalizeObjectConfigDataBody(
676 {
677 env,
678 extends: extend,
679 globals,
680 ignorePatterns,
681 noInlineConfig,
682 parser: parserName,
683 parserOptions,
684 plugins: pluginList,
685 processor,
686 reportUnusedDisableDirectives,
687 root,
688 rules,
689 settings,
690 overrides: overrideList = []
691 },
692 ctx
693 ) {
694 const extendList = Array.isArray(extend) ? extend : [extend];
695 const ignorePattern = ignorePatterns && new IgnorePattern(
696 Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns],
697 ctx.matchBasePath
698 );
699
700 // Flatten `extends`.
701 for (const extendName of extendList.filter(Boolean)) {
702 yield* this._loadExtends(extendName, ctx);
703 }
704
705 // Load parser & plugins.
706 const parser = parserName && this._loadParser(parserName, ctx);
707 const plugins = pluginList && this._loadPlugins(pluginList, ctx);
708
709 // Yield pseudo config data for file extension processors.
710 if (plugins) {
711 yield* this._takeFileExtensionProcessors(plugins, ctx);
712 }
713
714 // Yield the config data except `extends` and `overrides`.
715 yield {
716
717 // Debug information.
718 type: ctx.type,
719 name: ctx.name,
720 filePath: ctx.filePath,
721
722 // Config data.
723 criteria: null,
724 env,
725 globals,
726 ignorePattern,
727 noInlineConfig,
728 parser,
729 parserOptions,
730 plugins,
731 processor,
732 reportUnusedDisableDirectives,
733 root,
734 rules,
735 settings
736 };
737
738 // Flatten `overries`.
739 for (let i = 0; i < overrideList.length; ++i) {
740 yield* this._normalizeObjectConfigData(
741 overrideList[i],
742 { ...ctx, name: `${ctx.name}#overrides[${i}]` }
743 );
744 }
745 }
746
747 /**
748 * Load configs of an element in `extends`.
749 * @param {string} extendName The name of a base config.
750 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
751 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
752 * @private
753 */
754 _loadExtends(extendName, ctx) {
755 debug("Loading {extends:%j} relative to %s", extendName, ctx.filePath);
756 try {
757 if (extendName.startsWith("eslint:")) {
758 return this._loadExtendedBuiltInConfig(extendName, ctx);
759 }
760 if (extendName.startsWith("plugin:")) {
761 return this._loadExtendedPluginConfig(extendName, ctx);
762 }
763 return this._loadExtendedShareableConfig(extendName, ctx);
764 } catch (error) {
765 error.message += `\nReferenced from: ${ctx.filePath || ctx.name}`;
766 throw error;
767 }
768 }
769
770 /**
771 * Load configs of an element in `extends`.
772 * @param {string} extendName The name of a base config.
773 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
774 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
775 * @private
776 */
777 _loadExtendedBuiltInConfig(extendName, ctx) {
778 if (extendName === "eslint:recommended") {
779 return this._loadConfigData({
780 ...ctx,
781 filePath: eslintRecommendedPath,
782 name: `${ctx.name} » ${extendName}`
783 });
784 }
785 if (extendName === "eslint:all") {
786 return this._loadConfigData({
787 ...ctx,
788 filePath: eslintAllPath,
789 name: `${ctx.name} » ${extendName}`
790 });
791 }
792
793 throw configMissingError(extendName, ctx.name);
794 }
795
796 /**
797 * Load configs of an element in `extends`.
798 * @param {string} extendName The name of a base config.
799 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
800 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
801 * @private
802 */
803 _loadExtendedPluginConfig(extendName, ctx) {
804 const slashIndex = extendName.lastIndexOf("/");
805 const pluginName = extendName.slice("plugin:".length, slashIndex);
806 const configName = extendName.slice(slashIndex + 1);
807
808 if (isFilePath(pluginName)) {
809 throw new Error("'extends' cannot use a file path for plugins.");
810 }
811
812 const plugin = this._loadPlugin(pluginName, ctx);
813 const configData =
814 plugin.definition &&
815 plugin.definition.configs[configName];
816
817 if (configData) {
818 return this._normalizeConfigData(configData, {
819 ...ctx,
820 filePath: plugin.filePath,
821 name: `${ctx.name} » plugin:${plugin.id}/${configName}`
822 });
823 }
824
825 throw plugin.error || configMissingError(extendName, ctx.filePath);
826 }
827
828 /**
829 * Load configs of an element in `extends`.
830 * @param {string} extendName The name of a base config.
831 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
832 * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
833 * @private
834 */
835 _loadExtendedShareableConfig(extendName, ctx) {
836 const { cwd } = internalSlotsMap.get(this);
837 const relativeTo = ctx.filePath || path.join(cwd, "__placeholder__.js");
838 let request;
839
840 if (isFilePath(extendName)) {
841 request = extendName;
842 } else if (extendName.startsWith(".")) {
843 request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
844 } else {
845 request = naming.normalizePackageName(
846 extendName,
847 "eslint-config"
848 );
849 }
850
851 let filePath;
852
853 try {
854 filePath = ModuleResolver.resolve(request, relativeTo);
855 } catch (error) {
856 /* istanbul ignore else */
857 if (error && error.code === "MODULE_NOT_FOUND") {
858 throw configMissingError(extendName, ctx.filePath);
859 }
860 throw error;
861 }
862
863 writeDebugLogForLoading(request, relativeTo, filePath);
864 return this._loadConfigData({
865 ...ctx,
866 filePath,
867 name: `${ctx.name} » ${request}`
868 });
869 }
870
871 /**
872 * Load given plugins.
873 * @param {string[]} names The plugin names to load.
874 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
875 * @returns {Record<string,DependentPlugin>} The loaded parser.
876 * @private
877 */
878 _loadPlugins(names, ctx) {
879 return names.reduce((map, name) => {
880 if (isFilePath(name)) {
881 throw new Error("Plugins array cannot includes file paths.");
882 }
883 const plugin = this._loadPlugin(name, ctx);
884
885 map[plugin.id] = plugin;
886
887 return map;
888 }, {});
889 }
890
891 /**
892 * Load a given parser.
893 * @param {string} nameOrPath The package name or the path to a parser file.
894 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
895 * @returns {DependentParser} The loaded parser.
896 */
897 _loadParser(nameOrPath, ctx) {
898 debug("Loading parser %j from %s", nameOrPath, ctx.filePath);
899
900 const { cwd } = internalSlotsMap.get(this);
901 const relativeTo = ctx.filePath || path.join(cwd, "__placeholder__.js");
902
903 try {
904 const filePath = ModuleResolver.resolve(nameOrPath, relativeTo);
905
906 writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
907
908 return new ConfigDependency({
909 definition: require(filePath),
910 filePath,
911 id: nameOrPath,
912 importerName: ctx.name,
913 importerPath: ctx.filePath
914 });
915 } catch (error) {
916
917 // If the parser name is "espree", load the espree of ESLint.
918 if (nameOrPath === "espree") {
919 debug("Fallback espree.");
920 return new ConfigDependency({
921 definition: require("espree"),
922 filePath: require.resolve("espree"),
923 id: nameOrPath,
924 importerName: ctx.name,
925 importerPath: ctx.filePath
926 });
927 }
928
929 debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, ctx.name);
930 error.message = `Failed to load parser '${nameOrPath}' declared in '${ctx.name}': ${error.message}`;
931
932 return new ConfigDependency({
933 error,
934 id: nameOrPath,
935 importerName: ctx.name,
936 importerPath: ctx.filePath
937 });
938 }
939 }
940
941 /**
942 * Load a given plugin.
943 * @param {string} name The plugin name to load.
944 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
945 * @returns {DependentPlugin} The loaded plugin.
946 * @private
947 */
948 _loadPlugin(name, ctx) {
949 debug("Loading plugin %j from %s", name, ctx.filePath);
950
951 const { additionalPluginPool } = internalSlotsMap.get(this);
952 const request = naming.normalizePackageName(name, "eslint-plugin");
953 const id = naming.getShorthandName(request, "eslint-plugin");
954 const relativeTo = path.join(ctx.pluginBasePath, "__placeholder__.js");
955
956 if (name.match(/\s+/u)) {
957 const error = Object.assign(
958 new Error(`Whitespace found in plugin name '${name}'`),
959 {
960 messageTemplate: "whitespace-found",
961 messageData: { pluginName: request }
962 }
963 );
964
965 return new ConfigDependency({
966 error,
967 id,
968 importerName: ctx.name,
969 importerPath: ctx.filePath
970 });
971 }
972
973 // Check for additional pool.
974 const plugin =
975 additionalPluginPool.get(request) ||
976 additionalPluginPool.get(id);
977
978 if (plugin) {
979 return new ConfigDependency({
980 definition: normalizePlugin(plugin),
981 filePath: ctx.filePath,
982 id,
983 importerName: ctx.name,
984 importerPath: ctx.filePath
985 });
986 }
987
988 let filePath;
989 let error;
990
991 try {
992 filePath = ModuleResolver.resolve(request, relativeTo);
993 } catch (resolveError) {
994 error = resolveError;
995 /* istanbul ignore else */
996 if (error && error.code === "MODULE_NOT_FOUND") {
997 error.messageTemplate = "plugin-missing";
998 error.messageData = {
999 pluginName: request,
1000 resolvePluginsRelativeTo: ctx.pluginBasePath,
1001 importerName: ctx.name
1002 };
1003 }
1004 }
1005
1006 if (filePath) {
1007 try {
1008 writeDebugLogForLoading(request, relativeTo, filePath);
1009
1010 const startTime = Date.now();
1011 const pluginDefinition = require(filePath);
1012
1013 debug(`Plugin ${filePath} loaded in: ${Date.now() - startTime}ms`);
1014
1015 return new ConfigDependency({
1016 definition: normalizePlugin(pluginDefinition),
1017 filePath,
1018 id,
1019 importerName: ctx.name,
1020 importerPath: ctx.filePath
1021 });
1022 } catch (loadError) {
1023 error = loadError;
1024 }
1025 }
1026
1027 debug("Failed to load plugin '%s' declared in '%s'.", name, ctx.name);
1028 error.message = `Failed to load plugin '${name}' declared in '${ctx.name}': ${error.message}`;
1029 return new ConfigDependency({
1030 error,
1031 id,
1032 importerName: ctx.name,
1033 importerPath: ctx.filePath
1034 });
1035 }
1036
1037 /**
1038 * Take file expression processors as config array elements.
1039 * @param {Record<string,DependentPlugin>} plugins The plugin definitions.
1040 * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
1041 * @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
1042 * @private
1043 */
1044 *_takeFileExtensionProcessors(plugins, ctx) {
1045 for (const pluginId of Object.keys(plugins)) {
1046 const processors =
1047 plugins[pluginId] &&
1048 plugins[pluginId].definition &&
1049 plugins[pluginId].definition.processors;
1050
1051 if (!processors) {
1052 continue;
1053 }
1054
1055 for (const processorId of Object.keys(processors)) {
1056 if (processorId.startsWith(".")) {
1057 yield* this._normalizeObjectConfigData(
1058 {
1059 files: [`*${processorId}`],
1060 processor: `${pluginId}/${processorId}`
1061 },
1062 {
1063 ...ctx,
1064 type: "implicit-processor",
1065 name: `${ctx.name}#processors["${pluginId}/${processorId}"]`
1066 }
1067 );
1068 }
1069 }
1070 }
1071 }
1072 }
1073
1074 module.exports = { ConfigArrayFactory, createContext };