cascading-config-array-factory.js 17 KB

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