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