You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

490 lines
17 KiB

4 years ago
  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 { validateConfigArray } = 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 loadRules = require("./load-rules");
  31. const debug = require("debug")("eslint:cascading-config-array-factory");
  32. //------------------------------------------------------------------------------
  33. // Helpers
  34. //------------------------------------------------------------------------------
  35. // Define types for VSCode IntelliSense.
  36. /** @typedef {import("../shared/types").ConfigData} ConfigData */
  37. /** @typedef {import("../shared/types").Parser} Parser */
  38. /** @typedef {import("../shared/types").Plugin} Plugin */
  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. */
  51. /**
  52. * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
  53. * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
  54. * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
  55. * @property {ConfigArray} cliConfigArray The config array of CLI options.
  56. * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
  57. * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
  58. * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
  59. * @property {string} cwd The base directory to start lookup.
  60. * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
  61. * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
  62. * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
  63. * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
  64. * @property {boolean} useEslintrc if `false` then it doesn't load config files.
  65. */
  66. /** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
  67. const internalSlotsMap = new WeakMap();
  68. /**
  69. * Create the config array from `baseConfig` and `rulePaths`.
  70. * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
  71. * @returns {ConfigArray} The config array of the base configs.
  72. */
  73. function createBaseConfigArray({
  74. configArrayFactory,
  75. baseConfigData,
  76. rulePaths,
  77. cwd
  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. } = {}) {
  188. const configArrayFactory = new ConfigArrayFactory({
  189. additionalPluginPool,
  190. cwd,
  191. resolvePluginsRelativeTo
  192. });
  193. internalSlotsMap.set(this, {
  194. baseConfigArray: createBaseConfigArray({
  195. baseConfigData,
  196. configArrayFactory,
  197. cwd,
  198. rulePaths
  199. }),
  200. baseConfigData,
  201. cliConfigArray: createCLIConfigArray({
  202. cliConfigData,
  203. configArrayFactory,
  204. cwd,
  205. ignorePath,
  206. specificConfigPath
  207. }),
  208. cliConfigData,
  209. configArrayFactory,
  210. configCache: new Map(),
  211. cwd,
  212. finalizeCache: new WeakMap(),
  213. ignorePath,
  214. rulePaths,
  215. specificConfigPath,
  216. useEslintrc
  217. });
  218. }
  219. /**
  220. * The path to the current working directory.
  221. * This is used by tests.
  222. * @type {string}
  223. */
  224. get cwd() {
  225. const { cwd } = internalSlotsMap.get(this);
  226. return cwd;
  227. }
  228. /**
  229. * Get the config array of a given file.
  230. * If `filePath` was not given, it returns the config which contains only
  231. * `baseConfigData` and `cliConfigData`.
  232. * @param {string} [filePath] The file path to a file.
  233. * @param {Object} [options] The options.
  234. * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
  235. * @returns {ConfigArray} The config array of the file.
  236. */
  237. getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
  238. const {
  239. baseConfigArray,
  240. cliConfigArray,
  241. cwd
  242. } = internalSlotsMap.get(this);
  243. if (!filePath) {
  244. return new ConfigArray(...baseConfigArray, ...cliConfigArray);
  245. }
  246. const directoryPath = path.dirname(path.resolve(cwd, filePath));
  247. debug(`Load config files for ${directoryPath}.`);
  248. return this._finalizeConfigArray(
  249. this._loadConfigInAncestors(directoryPath),
  250. directoryPath,
  251. ignoreNotFoundError
  252. );
  253. }
  254. /**
  255. * Set the config data to override all configs.
  256. * Require to call `clearCache()` method after this method is called.
  257. * @param {ConfigData} configData The config data to override all configs.
  258. * @returns {void}
  259. */
  260. setOverrideConfig(configData) {
  261. const slots = internalSlotsMap.get(this);
  262. slots.cliConfigData = configData;
  263. }
  264. /**
  265. * Clear config cache.
  266. * @returns {void}
  267. */
  268. clearCache() {
  269. const slots = internalSlotsMap.get(this);
  270. slots.baseConfigArray = createBaseConfigArray(slots);
  271. slots.cliConfigArray = createCLIConfigArray(slots);
  272. slots.configCache.clear();
  273. }
  274. /**
  275. * Load and normalize config files from the ancestor directories.
  276. * @param {string} directoryPath The path to a leaf directory.
  277. * @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
  278. * @returns {ConfigArray} The loaded config.
  279. * @private
  280. */
  281. _loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
  282. const {
  283. baseConfigArray,
  284. configArrayFactory,
  285. configCache,
  286. cwd,
  287. useEslintrc
  288. } = internalSlotsMap.get(this);
  289. if (!useEslintrc) {
  290. return baseConfigArray;
  291. }
  292. let configArray = configCache.get(directoryPath);
  293. // Hit cache.
  294. if (configArray) {
  295. debug(`Cache hit: ${directoryPath}.`);
  296. return configArray;
  297. }
  298. debug(`No cache found: ${directoryPath}.`);
  299. const homePath = os.homedir();
  300. // Consider this is root.
  301. if (directoryPath === homePath && cwd !== homePath) {
  302. debug("Stop traversing because of considered root.");
  303. if (configsExistInSubdirs) {
  304. const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath);
  305. if (filePath) {
  306. emitDeprecationWarning(
  307. filePath,
  308. "ESLINT_PERSONAL_CONFIG_SUPPRESS"
  309. );
  310. }
  311. }
  312. return this._cacheConfig(directoryPath, baseConfigArray);
  313. }
  314. // Load the config on this directory.
  315. try {
  316. configArray = configArrayFactory.loadInDirectory(directoryPath);
  317. } catch (error) {
  318. /* istanbul ignore next */
  319. if (error.code === "EACCES") {
  320. debug("Stop traversing because of 'EACCES' error.");
  321. return this._cacheConfig(directoryPath, baseConfigArray);
  322. }
  323. throw error;
  324. }
  325. if (configArray.length > 0 && configArray.isRoot()) {
  326. debug("Stop traversing because of 'root:true'.");
  327. configArray.unshift(...baseConfigArray);
  328. return this._cacheConfig(directoryPath, configArray);
  329. }
  330. // Load from the ancestors and merge it.
  331. const parentPath = path.dirname(directoryPath);
  332. const parentConfigArray = parentPath && parentPath !== directoryPath
  333. ? this._loadConfigInAncestors(
  334. parentPath,
  335. configsExistInSubdirs || configArray.length > 0
  336. )
  337. : baseConfigArray;
  338. if (configArray.length > 0) {
  339. configArray.unshift(...parentConfigArray);
  340. } else {
  341. configArray = parentConfigArray;
  342. }
  343. // Cache and return.
  344. return this._cacheConfig(directoryPath, configArray);
  345. }
  346. /**
  347. * Freeze and cache a given config.
  348. * @param {string} directoryPath The path to a directory as a cache key.
  349. * @param {ConfigArray} configArray The config array as a cache value.
  350. * @returns {ConfigArray} The `configArray` (frozen).
  351. */
  352. _cacheConfig(directoryPath, configArray) {
  353. const { configCache } = internalSlotsMap.get(this);
  354. Object.freeze(configArray);
  355. configCache.set(directoryPath, configArray);
  356. return configArray;
  357. }
  358. /**
  359. * Finalize a given config array.
  360. * Concatenate `--config` and other CLI options.
  361. * @param {ConfigArray} configArray The parent config array.
  362. * @param {string} directoryPath The path to the leaf directory to find config files.
  363. * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
  364. * @returns {ConfigArray} The loaded config.
  365. * @private
  366. */
  367. _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
  368. const {
  369. cliConfigArray,
  370. configArrayFactory,
  371. finalizeCache,
  372. useEslintrc
  373. } = internalSlotsMap.get(this);
  374. let finalConfigArray = finalizeCache.get(configArray);
  375. if (!finalConfigArray) {
  376. finalConfigArray = configArray;
  377. // Load the personal config if there are no regular config files.
  378. if (
  379. useEslintrc &&
  380. configArray.every(c => !c.filePath) &&
  381. cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
  382. ) {
  383. const homePath = os.homedir();
  384. debug("Loading the config file of the home directory:", homePath);
  385. const personalConfigArray = configArrayFactory.loadInDirectory(
  386. homePath,
  387. { name: "PersonalConfig" }
  388. );
  389. if (
  390. personalConfigArray.length > 0 &&
  391. !directoryPath.startsWith(homePath)
  392. ) {
  393. const lastElement =
  394. personalConfigArray[personalConfigArray.length - 1];
  395. emitDeprecationWarning(
  396. lastElement.filePath,
  397. "ESLINT_PERSONAL_CONFIG_LOAD"
  398. );
  399. }
  400. finalConfigArray = finalConfigArray.concat(personalConfigArray);
  401. }
  402. // Apply CLI options.
  403. if (cliConfigArray.length > 0) {
  404. finalConfigArray = finalConfigArray.concat(cliConfigArray);
  405. }
  406. // Validate rule settings and environments.
  407. validateConfigArray(finalConfigArray);
  408. // Cache it.
  409. Object.freeze(finalConfigArray);
  410. finalizeCache.set(configArray, finalConfigArray);
  411. debug(
  412. "Configuration was determined: %o on %s",
  413. finalConfigArray,
  414. directoryPath
  415. );
  416. }
  417. // At least one element (the default ignore patterns) exists.
  418. if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
  419. throw new ConfigurationNotFoundError(directoryPath);
  420. }
  421. return finalConfigArray;
  422. }
  423. }
  424. //------------------------------------------------------------------------------
  425. // Public Interface
  426. //------------------------------------------------------------------------------
  427. module.exports = { CascadingConfigArrayFactory };