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 }; 503