cascading-config-array-factory.js 18 KB

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