UNPKG

17.5 kBJavaScriptView Raw
1/*
2 * STOP!!! DO NOT MODIFY.
3 *
4 * This file is part of the ongoing work to move the eslintrc-style config
5 * system into the @eslint/eslintrc package. This file needs to remain
6 * unchanged in order for this work to proceed.
7 *
8 * If you think you need to change this file, please contact @nzakas first.
9 *
10 * Thanks in advance for your cooperation.
11 */
12
13/**
14 * @fileoverview `CascadingConfigArrayFactory` class.
15 *
16 * `CascadingConfigArrayFactory` class has a responsibility:
17 *
18 * 1. Handles cascading of config files.
19 *
20 * It provides two methods:
21 *
22 * - `getConfigArrayForFile(filePath)`
23 * Get the corresponded configuration of a given file. This method doesn't
24 * throw even if the given file didn't exist.
25 * - `clearCache()`
26 * Clear the internal cache. You have to call this method when
27 * `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
28 * on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
29 *
30 * @author Toru Nagashima <https://github.com/mysticatea>
31 */
32"use strict";
33
34//------------------------------------------------------------------------------
35// Requirements
36//------------------------------------------------------------------------------
37
38const os = require("os");
39const path = require("path");
40const { validateConfigArray } = require("../shared/config-validator");
41const { emitDeprecationWarning } = require("../shared/deprecation-warnings");
42const { ConfigArrayFactory } = require("./config-array-factory");
43const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");
44const loadRules = require("./load-rules");
45const debug = require("debug")("eslint:cascading-config-array-factory");
46
47//------------------------------------------------------------------------------
48// Helpers
49//------------------------------------------------------------------------------
50
51// Define types for VSCode IntelliSense.
52/** @typedef {import("../shared/types").ConfigData} ConfigData */
53/** @typedef {import("../shared/types").Parser} Parser */
54/** @typedef {import("../shared/types").Plugin} Plugin */
55/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
56
57/**
58 * @typedef {Object} CascadingConfigArrayFactoryOptions
59 * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
60 * @property {ConfigData} [baseConfig] The config by `baseConfig` option.
61 * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--ignore-pattern`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
62 * @property {string} [cwd] The base directory to start lookup.
63 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
64 * @property {string[]} [rulePaths] The value of `--rulesdir` option.
65 * @property {string} [specificConfigPath] The value of `--config` option.
66 * @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
67 */
68
69/**
70 * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
71 * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
72 * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
73 * @property {ConfigArray} cliConfigArray The config array of CLI options.
74 * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
75 * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
76 * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
77 * @property {string} cwd The base directory to start lookup.
78 * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
79 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
80 * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
81 * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
82 * @property {boolean} useEslintrc if `false` then it doesn't load config files.
83 */
84
85/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
86const internalSlotsMap = new WeakMap();
87
88/**
89 * Create the config array from `baseConfig` and `rulePaths`.
90 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
91 * @returns {ConfigArray} The config array of the base configs.
92 */
93function createBaseConfigArray({
94 configArrayFactory,
95 baseConfigData,
96 rulePaths,
97 cwd
98}) {
99 const baseConfigArray = configArrayFactory.create(
100 baseConfigData,
101 { name: "BaseConfig" }
102 );
103
104 /*
105 * Create the config array element for the default ignore patterns.
106 * This element has `ignorePattern` property that ignores the default
107 * patterns in the current working directory.
108 */
109 baseConfigArray.unshift(configArrayFactory.create(
110 { ignorePatterns: IgnorePattern.DefaultPatterns },
111 { name: "DefaultIgnorePattern" }
112 )[0]);
113
114 /*
115 * Load rules `--rulesdir` option as a pseudo plugin.
116 * Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
117 * the rule's options with only information in the config array.
118 */
119 if (rulePaths && rulePaths.length > 0) {
120 baseConfigArray.push({
121 type: "config",
122 name: "--rulesdir",
123 filePath: "",
124 plugins: {
125 "": new ConfigDependency({
126 definition: {
127 rules: rulePaths.reduce(
128 (map, rulesPath) => Object.assign(
129 map,
130 loadRules(rulesPath, cwd)
131 ),
132 {}
133 )
134 },
135 filePath: "",
136 id: "",
137 importerName: "--rulesdir",
138 importerPath: ""
139 })
140 }
141 });
142 }
143
144 return baseConfigArray;
145}
146
147/**
148 * Create the config array from CLI options.
149 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
150 * @returns {ConfigArray} The config array of the base configs.
151 */
152function createCLIConfigArray({
153 cliConfigData,
154 configArrayFactory,
155 cwd,
156 ignorePath,
157 specificConfigPath
158}) {
159 const cliConfigArray = configArrayFactory.create(
160 cliConfigData,
161 { name: "CLIOptions" }
162 );
163
164 cliConfigArray.unshift(
165 ...(ignorePath
166 ? configArrayFactory.loadESLintIgnore(ignorePath)
167 : configArrayFactory.loadDefaultESLintIgnore())
168 );
169
170 if (specificConfigPath) {
171 cliConfigArray.unshift(
172 ...configArrayFactory.loadFile(
173 specificConfigPath,
174 { name: "--config", basePath: cwd }
175 )
176 );
177 }
178
179 return cliConfigArray;
180}
181
182/**
183 * The error type when there are files matched by a glob, but all of them have been ignored.
184 */
185class ConfigurationNotFoundError extends Error {
186
187 // eslint-disable-next-line jsdoc/require-description
188 /**
189 * @param {string} directoryPath The directory path.
190 */
191 constructor(directoryPath) {
192 super(`No ESLint configuration found in ${directoryPath}.`);
193 this.messageTemplate = "no-config-found";
194 this.messageData = { directoryPath };
195 }
196}
197
198/**
199 * This class provides the functionality that enumerates every file which is
200 * matched by given glob patterns and that configuration.
201 */
202class CascadingConfigArrayFactory {
203
204 /**
205 * Initialize this enumerator.
206 * @param {CascadingConfigArrayFactoryOptions} options The options.
207 */
208 constructor({
209 additionalPluginPool = new Map(),
210 baseConfig: baseConfigData = null,
211 cliConfig: cliConfigData = null,
212 cwd = process.cwd(),
213 ignorePath,
214 resolvePluginsRelativeTo,
215 rulePaths = [],
216 specificConfigPath = null,
217 useEslintrc = true
218 } = {}) {
219 const configArrayFactory = new ConfigArrayFactory({
220 additionalPluginPool,
221 cwd,
222 resolvePluginsRelativeTo
223 });
224
225 internalSlotsMap.set(this, {
226 baseConfigArray: createBaseConfigArray({
227 baseConfigData,
228 configArrayFactory,
229 cwd,
230 rulePaths
231 }),
232 baseConfigData,
233 cliConfigArray: createCLIConfigArray({
234 cliConfigData,
235 configArrayFactory,
236 cwd,
237 ignorePath,
238 specificConfigPath
239 }),
240 cliConfigData,
241 configArrayFactory,
242 configCache: new Map(),
243 cwd,
244 finalizeCache: new WeakMap(),
245 ignorePath,
246 rulePaths,
247 specificConfigPath,
248 useEslintrc
249 });
250 }
251
252 /**
253 * The path to the current working directory.
254 * This is used by tests.
255 * @type {string}
256 */
257 get cwd() {
258 const { cwd } = internalSlotsMap.get(this);
259
260 return cwd;
261 }
262
263 /**
264 * Get the config array of a given file.
265 * If `filePath` was not given, it returns the config which contains only
266 * `baseConfigData` and `cliConfigData`.
267 * @param {string} [filePath] The file path to a file.
268 * @param {Object} [options] The options.
269 * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
270 * @returns {ConfigArray} The config array of the file.
271 */
272 getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
273 const {
274 baseConfigArray,
275 cliConfigArray,
276 cwd
277 } = internalSlotsMap.get(this);
278
279 if (!filePath) {
280 return new ConfigArray(...baseConfigArray, ...cliConfigArray);
281 }
282
283 const directoryPath = path.dirname(path.resolve(cwd, filePath));
284
285 debug(`Load config files for ${directoryPath}.`);
286
287 return this._finalizeConfigArray(
288 this._loadConfigInAncestors(directoryPath),
289 directoryPath,
290 ignoreNotFoundError
291 );
292 }
293
294 /**
295 * Set the config data to override all configs.
296 * Require to call `clearCache()` method after this method is called.
297 * @param {ConfigData} configData The config data to override all configs.
298 * @returns {void}
299 */
300 setOverrideConfig(configData) {
301 const slots = internalSlotsMap.get(this);
302
303 slots.cliConfigData = configData;
304 }
305
306 /**
307 * Clear config cache.
308 * @returns {void}
309 */
310 clearCache() {
311 const slots = internalSlotsMap.get(this);
312
313 slots.baseConfigArray = createBaseConfigArray(slots);
314 slots.cliConfigArray = createCLIConfigArray(slots);
315 slots.configCache.clear();
316 }
317
318 /**
319 * Load and normalize config files from the ancestor directories.
320 * @param {string} directoryPath The path to a leaf directory.
321 * @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
322 * @returns {ConfigArray} The loaded config.
323 * @private
324 */
325 _loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
326 const {
327 baseConfigArray,
328 configArrayFactory,
329 configCache,
330 cwd,
331 useEslintrc
332 } = internalSlotsMap.get(this);
333
334 if (!useEslintrc) {
335 return baseConfigArray;
336 }
337
338 let configArray = configCache.get(directoryPath);
339
340 // Hit cache.
341 if (configArray) {
342 debug(`Cache hit: ${directoryPath}.`);
343 return configArray;
344 }
345 debug(`No cache found: ${directoryPath}.`);
346
347 const homePath = os.homedir();
348
349 // Consider this is root.
350 if (directoryPath === homePath && cwd !== homePath) {
351 debug("Stop traversing because of considered root.");
352 if (configsExistInSubdirs) {
353 const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath);
354
355 if (filePath) {
356 emitDeprecationWarning(
357 filePath,
358 "ESLINT_PERSONAL_CONFIG_SUPPRESS"
359 );
360 }
361 }
362 return this._cacheConfig(directoryPath, baseConfigArray);
363 }
364
365 // Load the config on this directory.
366 try {
367 configArray = configArrayFactory.loadInDirectory(directoryPath);
368 } catch (error) {
369 /* istanbul ignore next */
370 if (error.code === "EACCES") {
371 debug("Stop traversing because of 'EACCES' error.");
372 return this._cacheConfig(directoryPath, baseConfigArray);
373 }
374 throw error;
375 }
376
377 if (configArray.length > 0 && configArray.isRoot()) {
378 debug("Stop traversing because of 'root:true'.");
379 configArray.unshift(...baseConfigArray);
380 return this._cacheConfig(directoryPath, configArray);
381 }
382
383 // Load from the ancestors and merge it.
384 const parentPath = path.dirname(directoryPath);
385 const parentConfigArray = parentPath && parentPath !== directoryPath
386 ? this._loadConfigInAncestors(
387 parentPath,
388 configsExistInSubdirs || configArray.length > 0
389 )
390 : baseConfigArray;
391
392 if (configArray.length > 0) {
393 configArray.unshift(...parentConfigArray);
394 } else {
395 configArray = parentConfigArray;
396 }
397
398 // Cache and return.
399 return this._cacheConfig(directoryPath, configArray);
400 }
401
402 /**
403 * Freeze and cache a given config.
404 * @param {string} directoryPath The path to a directory as a cache key.
405 * @param {ConfigArray} configArray The config array as a cache value.
406 * @returns {ConfigArray} The `configArray` (frozen).
407 */
408 _cacheConfig(directoryPath, configArray) {
409 const { configCache } = internalSlotsMap.get(this);
410
411 Object.freeze(configArray);
412 configCache.set(directoryPath, configArray);
413
414 return configArray;
415 }
416
417 /**
418 * Finalize a given config array.
419 * Concatenate `--config` and other CLI options.
420 * @param {ConfigArray} configArray The parent config array.
421 * @param {string} directoryPath The path to the leaf directory to find config files.
422 * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
423 * @returns {ConfigArray} The loaded config.
424 * @private
425 */
426 _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
427 const {
428 cliConfigArray,
429 configArrayFactory,
430 finalizeCache,
431 useEslintrc
432 } = internalSlotsMap.get(this);
433
434 let finalConfigArray = finalizeCache.get(configArray);
435
436 if (!finalConfigArray) {
437 finalConfigArray = configArray;
438
439 // Load the personal config if there are no regular config files.
440 if (
441 useEslintrc &&
442 configArray.every(c => !c.filePath) &&
443 cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
444 ) {
445 const homePath = os.homedir();
446
447 debug("Loading the config file of the home directory:", homePath);
448
449 const personalConfigArray = configArrayFactory.loadInDirectory(
450 homePath,
451 { name: "PersonalConfig" }
452 );
453
454 if (
455 personalConfigArray.length > 0 &&
456 !directoryPath.startsWith(homePath)
457 ) {
458 const lastElement =
459 personalConfigArray[personalConfigArray.length - 1];
460
461 emitDeprecationWarning(
462 lastElement.filePath,
463 "ESLINT_PERSONAL_CONFIG_LOAD"
464 );
465 }
466
467 finalConfigArray = finalConfigArray.concat(personalConfigArray);
468 }
469
470 // Apply CLI options.
471 if (cliConfigArray.length > 0) {
472 finalConfigArray = finalConfigArray.concat(cliConfigArray);
473 }
474
475 // Validate rule settings and environments.
476 validateConfigArray(finalConfigArray);
477
478 // Cache it.
479 Object.freeze(finalConfigArray);
480 finalizeCache.set(configArray, finalConfigArray);
481
482 debug(
483 "Configuration was determined: %o on %s",
484 finalConfigArray,
485 directoryPath
486 );
487 }
488
489 // At least one element (the default ignore patterns) exists.
490 if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
491 throw new ConfigurationNotFoundError(directoryPath);
492 }
493
494 return finalConfigArray;
495 }
496}
497
498//------------------------------------------------------------------------------
499// Public Interface
500//------------------------------------------------------------------------------
501
502module.exports = { CascadingConfigArrayFactory };