UNPKG

14.1 kBJavaScriptView Raw
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
26const os = require("os");
27const path = require("path");
28const { validateConfigArray } = require("../shared/config-validator");
29const { ConfigArrayFactory } = require("./config-array-factory");
30const { ConfigArray, ConfigDependency } = require("./config-array");
31const loadRules = require("./load-rules");
32const debug = require("debug")("eslint:cascading-config-array-factory");
33
34//------------------------------------------------------------------------------
35// Helpers
36//------------------------------------------------------------------------------
37
38// Define types for VSCode IntelliSense.
39/** @typedef {import("../shared/types").ConfigData} ConfigData */
40/** @typedef {import("../shared/types").Parser} Parser */
41/** @typedef {import("../shared/types").Plugin} Plugin */
42/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
43
44/**
45 * @typedef {Object} CascadingConfigArrayFactoryOptions
46 * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
47 * @property {ConfigData} [baseConfig] The config by `baseConfig` option.
48 * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
49 * @property {string} [cwd] The base directory to start lookup.
50 * @property {string[]} [rulePaths] The value of `--rulesdir` option.
51 * @property {string} [specificConfigPath] The value of `--config` option.
52 * @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
53 */
54
55/**
56 * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
57 * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
58 * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
59 * @property {ConfigArray} cliConfigArray The config array of CLI options.
60 * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
61 * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
62 * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
63 * @property {string} cwd The base directory to start lookup.
64 * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
65 * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
66 * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
67 * @property {boolean} useEslintrc if `false` then it doesn't load config files.
68 */
69
70/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
71const internalSlotsMap = new WeakMap();
72
73/**
74 * Create the config array from `baseConfig` and `rulePaths`.
75 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
76 * @returns {ConfigArray} The config array of the base configs.
77 */
78function createBaseConfigArray({
79 configArrayFactory,
80 baseConfigData,
81 rulePaths,
82 cwd
83}) {
84 const baseConfigArray = configArrayFactory.create(
85 baseConfigData,
86 { name: "BaseConfig" }
87 );
88
89 if (rulePaths && rulePaths.length > 0) {
90
91 /*
92 * Load rules `--rulesdir` option as a pseudo plugin.
93 * Use a pseudo plugin to define rules of `--rulesdir`, so we can
94 * validate the rule's options with only information in the config
95 * array.
96 */
97 baseConfigArray.push({
98 name: "--rulesdir",
99 filePath: "",
100 plugins: {
101 "": new ConfigDependency({
102 definition: {
103 rules: rulePaths.reduce(
104 (map, rulesPath) => Object.assign(
105 map,
106 loadRules(rulesPath, cwd)
107 ),
108 {}
109 )
110 },
111 filePath: "",
112 id: "",
113 importerName: "--rulesdir",
114 importerPath: ""
115 })
116 }
117 });
118 }
119
120 return baseConfigArray;
121}
122
123/**
124 * Create the config array from CLI options.
125 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
126 * @returns {ConfigArray} The config array of the base configs.
127 */
128function createCLIConfigArray({
129 cliConfigData,
130 configArrayFactory,
131 specificConfigPath
132}) {
133 const cliConfigArray = configArrayFactory.create(
134 cliConfigData,
135 { name: "CLIOptions" }
136 );
137
138 if (specificConfigPath) {
139 cliConfigArray.unshift(
140 ...configArrayFactory.loadFile(
141 specificConfigPath,
142 { name: "--config" }
143 )
144 );
145 }
146
147 return cliConfigArray;
148}
149
150/**
151 * The error type when there are files matched by a glob, but all of them have been ignored.
152 */
153class ConfigurationNotFoundError extends Error {
154
155 /**
156 * @param {string} directoryPath - The directory path.
157 */
158 constructor(directoryPath) {
159 super(`No ESLint configuration found in ${directoryPath}.`);
160 this.messageTemplate = "no-config-found";
161 this.messageData = { directoryPath };
162 }
163}
164
165/**
166 * This class provides the functionality that enumerates every file which is
167 * matched by given glob patterns and that configuration.
168 */
169class CascadingConfigArrayFactory {
170
171 /**
172 * Initialize this enumerator.
173 * @param {CascadingConfigArrayFactoryOptions} options The options.
174 */
175 constructor({
176 additionalPluginPool = new Map(),
177 baseConfig: baseConfigData = null,
178 cliConfig: cliConfigData = null,
179 cwd = process.cwd(),
180 resolvePluginsRelativeTo = cwd,
181 rulePaths = [],
182 specificConfigPath = null,
183 useEslintrc = true
184 } = {}) {
185 const configArrayFactory = new ConfigArrayFactory({
186 additionalPluginPool,
187 cwd,
188 resolvePluginsRelativeTo
189 });
190
191 internalSlotsMap.set(this, {
192 baseConfigArray: createBaseConfigArray({
193 baseConfigData,
194 configArrayFactory,
195 cwd,
196 rulePaths
197 }),
198 baseConfigData,
199 cliConfigArray: createCLIConfigArray({
200 cliConfigData,
201 configArrayFactory,
202 specificConfigPath
203 }),
204 cliConfigData,
205 configArrayFactory,
206 configCache: new Map(),
207 cwd,
208 finalizeCache: new WeakMap(),
209 rulePaths,
210 specificConfigPath,
211 useEslintrc
212 });
213 }
214
215 /**
216 * The path to the current working directory.
217 * This is used by tests.
218 * @type {string}
219 */
220 get cwd() {
221 const { cwd } = internalSlotsMap.get(this);
222
223 return cwd;
224 }
225
226 /**
227 * Get the config array of a given file.
228 * If `filePath` was not given, it returns the config which contains only
229 * `baseConfigData` and `cliConfigData`.
230 * @param {string} [filePath] The file path to a file.
231 * @returns {ConfigArray} The config array of the file.
232 */
233 getConfigArrayForFile(filePath) {
234 const {
235 baseConfigArray,
236 cliConfigArray,
237 cwd
238 } = internalSlotsMap.get(this);
239
240 if (!filePath) {
241 return new ConfigArray(...baseConfigArray, ...cliConfigArray);
242 }
243
244 const directoryPath = path.dirname(path.resolve(cwd, filePath));
245
246 debug(`Load config files for ${directoryPath}.`);
247
248 return this._finalizeConfigArray(
249 this._loadConfigInAncestors(directoryPath),
250 directoryPath
251 );
252 }
253
254 /**
255 * Clear config cache.
256 * @returns {void}
257 */
258 clearCache() {
259 const slots = internalSlotsMap.get(this);
260
261 slots.baseConfigArray = createBaseConfigArray(slots);
262 slots.cliConfigArray = createCLIConfigArray(slots);
263 slots.configCache.clear();
264 }
265
266 /**
267 * Load and normalize config files from the ancestor directories.
268 * @param {string} directoryPath The path to a leaf directory.
269 * @returns {ConfigArray} The loaded config.
270 * @private
271 */
272 _loadConfigInAncestors(directoryPath) {
273 const {
274 baseConfigArray,
275 configArrayFactory,
276 configCache,
277 cwd,
278 useEslintrc
279 } = internalSlotsMap.get(this);
280
281 if (!useEslintrc) {
282 return baseConfigArray;
283 }
284
285 let configArray = configCache.get(directoryPath);
286
287 // Hit cache.
288 if (configArray) {
289 debug(`Cache hit: ${directoryPath}.`);
290 return configArray;
291 }
292 debug(`No cache found: ${directoryPath}.`);
293
294 const homePath = os.homedir();
295
296 // Consider this is root.
297 if (directoryPath === homePath && cwd !== homePath) {
298 debug("Stop traversing because of considered root.");
299 return this._cacheConfig(directoryPath, baseConfigArray);
300 }
301
302 // Load the config on this directory.
303 try {
304 configArray = configArrayFactory.loadInDirectory(directoryPath);
305 } catch (error) {
306 /* istanbul ignore next */
307 if (error.code === "EACCES") {
308 debug("Stop traversing because of 'EACCES' error.");
309 return this._cacheConfig(directoryPath, baseConfigArray);
310 }
311 throw error;
312 }
313
314 if (configArray.length > 0 && configArray.isRoot()) {
315 debug("Stop traversing because of 'root:true'.");
316 configArray.unshift(...baseConfigArray);
317 return this._cacheConfig(directoryPath, configArray);
318 }
319
320 // Load from the ancestors and merge it.
321 const parentPath = path.dirname(directoryPath);
322 const parentConfigArray = parentPath && parentPath !== directoryPath
323 ? this._loadConfigInAncestors(parentPath)
324 : baseConfigArray;
325
326 if (configArray.length > 0) {
327 configArray.unshift(...parentConfigArray);
328 } else {
329 configArray = parentConfigArray;
330 }
331
332 // Cache and return.
333 return this._cacheConfig(directoryPath, configArray);
334 }
335
336 /**
337 * Freeze and cache a given config.
338 * @param {string} directoryPath The path to a directory as a cache key.
339 * @param {ConfigArray} configArray The config array as a cache value.
340 * @returns {ConfigArray} The `configArray` (frozen).
341 */
342 _cacheConfig(directoryPath, configArray) {
343 const { configCache } = internalSlotsMap.get(this);
344
345 Object.freeze(configArray);
346 configCache.set(directoryPath, configArray);
347
348 return configArray;
349 }
350
351 /**
352 * Finalize a given config array.
353 * Concatenate `--config` and other CLI options.
354 * @param {ConfigArray} configArray The parent config array.
355 * @param {string} directoryPath The path to the leaf directory to find config files.
356 * @returns {ConfigArray} The loaded config.
357 * @private
358 */
359 _finalizeConfigArray(configArray, directoryPath) {
360 const {
361 cliConfigArray,
362 configArrayFactory,
363 finalizeCache,
364 useEslintrc
365 } = internalSlotsMap.get(this);
366
367 let finalConfigArray = finalizeCache.get(configArray);
368
369 if (!finalConfigArray) {
370 finalConfigArray = configArray;
371
372 // Load the personal config if there are no regular config files.
373 if (
374 useEslintrc &&
375 configArray.every(c => !c.filePath) &&
376 cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
377 ) {
378 debug("Loading the config file of the home directory.");
379
380 finalConfigArray = configArrayFactory.loadInDirectory(
381 os.homedir(),
382 { name: "PersonalConfig", parent: finalConfigArray }
383 );
384 }
385
386 // Apply CLI options.
387 if (cliConfigArray.length > 0) {
388 finalConfigArray = finalConfigArray.concat(cliConfigArray);
389 }
390
391 // Validate rule settings and environments.
392 validateConfigArray(finalConfigArray);
393
394 // Cache it.
395 Object.freeze(finalConfigArray);
396 finalizeCache.set(configArray, finalConfigArray);
397
398 debug(
399 "Configuration was determined: %o on %s",
400 finalConfigArray,
401 directoryPath
402 );
403 }
404
405 if (useEslintrc && finalConfigArray.length === 0) {
406 throw new ConfigurationNotFoundError(directoryPath);
407 }
408
409 return finalConfigArray;
410 }
411}
412
413//------------------------------------------------------------------------------
414// Public Interface
415//------------------------------------------------------------------------------
416
417module.exports = { CascadingConfigArrayFactory };