cascading-config-array-factory.js 18 KB

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