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 | ;
|
33 |
|
34 | //------------------------------------------------------------------------------
|
35 | // Requirements
|
36 | //------------------------------------------------------------------------------
|
37 |
|
38 | const os = require("os");
|
39 | const path = require("path");
|
40 | const { validateConfigArray } = require("../shared/config-validator");
|
41 | const { emitDeprecationWarning } = require("../shared/deprecation-warnings");
|
42 | const { ConfigArrayFactory } = require("./config-array-factory");
|
43 | const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");
|
44 | const loadRules = require("./load-rules");
|
45 | const 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>} */
|
86 | const 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 | */
|
93 | function 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 | */
|
152 | function 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 | */
|
185 | class 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 | */
|
202 | class 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 |
|
502 | module.exports = { CascadingConfigArrayFactory };
|