config-array-factory.js 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039
  1. /**
  2. * @fileoverview The factory of `ConfigArray` objects.
  3. *
  4. * This class provides methods to create `ConfigArray` instance.
  5. *
  6. * - `create(configData, options)`
  7. * Create a `ConfigArray` instance from a config data. This is to handle CLI
  8. * options except `--config`.
  9. * - `loadFile(filePath, options)`
  10. * Create a `ConfigArray` instance from a config file. This is to handle
  11. * `--config` option. If the file was not found, throws the following error:
  12. * - If the filename was `*.js`, a `MODULE_NOT_FOUND` error.
  13. * - If the filename was `package.json`, an IO error or an
  14. * `ESLINT_CONFIG_FIELD_NOT_FOUND` error.
  15. * - Otherwise, an IO error such as `ENOENT`.
  16. * - `loadInDirectory(directoryPath, options)`
  17. * Create a `ConfigArray` instance from a config file which is on a given
  18. * directory. This tries to load `.eslintrc.*` or `package.json`. If not
  19. * found, returns an empty `ConfigArray`.
  20. * - `loadESLintIgnore(filePath)`
  21. * Create a `ConfigArray` instance from a config file that is `.eslintignore`
  22. * format. This is to handle `--ignore-path` option.
  23. * - `loadDefaultESLintIgnore()`
  24. * Create a `ConfigArray` instance from `.eslintignore` or `package.json` in
  25. * the current working directory.
  26. *
  27. * `ConfigArrayFactory` class has the responsibility that loads configuration
  28. * files, including loading `extends`, `parser`, and `plugins`. The created
  29. * `ConfigArray` instance has the loaded `extends`, `parser`, and `plugins`.
  30. *
  31. * But this class doesn't handle cascading. `CascadingConfigArrayFactory` class
  32. * handles cascading and hierarchy.
  33. *
  34. * @author Toru Nagashima <https://github.com/mysticatea>
  35. */
  36. "use strict";
  37. //------------------------------------------------------------------------------
  38. // Requirements
  39. //------------------------------------------------------------------------------
  40. const fs = require("fs");
  41. const path = require("path");
  42. const importFresh = require("import-fresh");
  43. const stripComments = require("strip-json-comments");
  44. const { validateConfigSchema } = require("../shared/config-validator");
  45. const naming = require("../shared/naming");
  46. const ModuleResolver = require("../shared/relative-module-resolver");
  47. const {
  48. ConfigArray,
  49. ConfigDependency,
  50. IgnorePattern,
  51. OverrideTester
  52. } = require("./config-array");
  53. const debug = require("debug")("eslint:config-array-factory");
  54. //------------------------------------------------------------------------------
  55. // Helpers
  56. //------------------------------------------------------------------------------
  57. const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
  58. const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js");
  59. const configFilenames = [
  60. ".eslintrc.js",
  61. ".eslintrc.cjs",
  62. ".eslintrc.yaml",
  63. ".eslintrc.yml",
  64. ".eslintrc.json",
  65. ".eslintrc",
  66. "package.json"
  67. ];
  68. // Define types for VSCode IntelliSense.
  69. /** @typedef {import("../shared/types").ConfigData} ConfigData */
  70. /** @typedef {import("../shared/types").OverrideConfigData} OverrideConfigData */
  71. /** @typedef {import("../shared/types").Parser} Parser */
  72. /** @typedef {import("../shared/types").Plugin} Plugin */
  73. /** @typedef {import("./config-array/config-dependency").DependentParser} DependentParser */
  74. /** @typedef {import("./config-array/config-dependency").DependentPlugin} DependentPlugin */
  75. /** @typedef {ConfigArray[0]} ConfigArrayElement */
  76. /**
  77. * @typedef {Object} ConfigArrayFactoryOptions
  78. * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
  79. * @property {string} [cwd] The path to the current working directory.
  80. * @property {string} [resolvePluginsRelativeTo] A path to the directory that plugins should be resolved from. Defaults to `cwd`.
  81. */
  82. /**
  83. * @typedef {Object} ConfigArrayFactoryInternalSlots
  84. * @property {Map<string,Plugin>} additionalPluginPool The map for additional plugins.
  85. * @property {string} cwd The path to the current working directory.
  86. * @property {string} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
  87. */
  88. /** @type {WeakMap<ConfigArrayFactory, ConfigArrayFactoryInternalSlots>} */
  89. const internalSlotsMap = new WeakMap();
  90. /**
  91. * Check if a given string is a file path.
  92. * @param {string} nameOrPath A module name or file path.
  93. * @returns {boolean} `true` if the `nameOrPath` is a file path.
  94. */
  95. function isFilePath(nameOrPath) {
  96. return (
  97. /^\.{1,2}[/\\]/u.test(nameOrPath) ||
  98. path.isAbsolute(nameOrPath)
  99. );
  100. }
  101. /**
  102. * Convenience wrapper for synchronously reading file contents.
  103. * @param {string} filePath The filename to read.
  104. * @returns {string} The file contents, with the BOM removed.
  105. * @private
  106. */
  107. function readFile(filePath) {
  108. return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/u, "");
  109. }
  110. /**
  111. * Loads a YAML configuration from a file.
  112. * @param {string} filePath The filename to load.
  113. * @returns {ConfigData} The configuration object from the file.
  114. * @throws {Error} If the file cannot be read.
  115. * @private
  116. */
  117. function loadYAMLConfigFile(filePath) {
  118. debug(`Loading YAML config file: ${filePath}`);
  119. // lazy load YAML to improve performance when not used
  120. const yaml = require("js-yaml");
  121. try {
  122. // empty YAML file can be null, so always use
  123. return yaml.safeLoad(readFile(filePath)) || {};
  124. } catch (e) {
  125. debug(`Error reading YAML file: ${filePath}`);
  126. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  127. throw e;
  128. }
  129. }
  130. /**
  131. * Loads a JSON configuration from a file.
  132. * @param {string} filePath The filename to load.
  133. * @returns {ConfigData} The configuration object from the file.
  134. * @throws {Error} If the file cannot be read.
  135. * @private
  136. */
  137. function loadJSONConfigFile(filePath) {
  138. debug(`Loading JSON config file: ${filePath}`);
  139. try {
  140. return JSON.parse(stripComments(readFile(filePath)));
  141. } catch (e) {
  142. debug(`Error reading JSON file: ${filePath}`);
  143. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  144. e.messageTemplate = "failed-to-read-json";
  145. e.messageData = {
  146. path: filePath,
  147. message: e.message
  148. };
  149. throw e;
  150. }
  151. }
  152. /**
  153. * Loads a legacy (.eslintrc) configuration from a file.
  154. * @param {string} filePath The filename to load.
  155. * @returns {ConfigData} The configuration object from the file.
  156. * @throws {Error} If the file cannot be read.
  157. * @private
  158. */
  159. function loadLegacyConfigFile(filePath) {
  160. debug(`Loading legacy config file: ${filePath}`);
  161. // lazy load YAML to improve performance when not used
  162. const yaml = require("js-yaml");
  163. try {
  164. return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
  165. } catch (e) {
  166. debug("Error reading YAML file: %s\n%o", filePath, e);
  167. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  168. throw e;
  169. }
  170. }
  171. /**
  172. * Loads a JavaScript configuration from a file.
  173. * @param {string} filePath The filename to load.
  174. * @returns {ConfigData} The configuration object from the file.
  175. * @throws {Error} If the file cannot be read.
  176. * @private
  177. */
  178. function loadJSConfigFile(filePath) {
  179. debug(`Loading JS config file: ${filePath}`);
  180. try {
  181. return importFresh(filePath);
  182. } catch (e) {
  183. debug(`Error reading JavaScript file: ${filePath}`);
  184. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  185. throw e;
  186. }
  187. }
  188. /**
  189. * Loads a configuration from a package.json file.
  190. * @param {string} filePath The filename to load.
  191. * @returns {ConfigData} The configuration object from the file.
  192. * @throws {Error} If the file cannot be read.
  193. * @private
  194. */
  195. function loadPackageJSONConfigFile(filePath) {
  196. debug(`Loading package.json config file: ${filePath}`);
  197. try {
  198. const packageData = loadJSONConfigFile(filePath);
  199. if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) {
  200. throw Object.assign(
  201. new Error("package.json file doesn't have 'eslintConfig' field."),
  202. { code: "ESLINT_CONFIG_FIELD_NOT_FOUND" }
  203. );
  204. }
  205. return packageData.eslintConfig;
  206. } catch (e) {
  207. debug(`Error reading package.json file: ${filePath}`);
  208. e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
  209. throw e;
  210. }
  211. }
  212. /**
  213. * Loads a `.eslintignore` from a file.
  214. * @param {string} filePath The filename to load.
  215. * @returns {string[]} The ignore patterns from the file.
  216. * @private
  217. */
  218. function loadESLintIgnoreFile(filePath) {
  219. debug(`Loading .eslintignore file: ${filePath}`);
  220. try {
  221. return readFile(filePath)
  222. .split(/\r?\n/gu)
  223. .filter(line => line.trim() !== "" && !line.startsWith("#"));
  224. } catch (e) {
  225. debug(`Error reading .eslintignore file: ${filePath}`);
  226. e.message = `Cannot read .eslintignore file: ${filePath}\nError: ${e.message}`;
  227. throw e;
  228. }
  229. }
  230. /**
  231. * Creates an error to notify about a missing config to extend from.
  232. * @param {string} configName The name of the missing config.
  233. * @param {string} importerName The name of the config that imported the missing config
  234. * @returns {Error} The error object to throw
  235. * @private
  236. */
  237. function configMissingError(configName, importerName) {
  238. return Object.assign(
  239. new Error(`Failed to load config "${configName}" to extend from.`),
  240. {
  241. messageTemplate: "extend-config-missing",
  242. messageData: { configName, importerName }
  243. }
  244. );
  245. }
  246. /**
  247. * Loads a configuration file regardless of the source. Inspects the file path
  248. * to determine the correctly way to load the config file.
  249. * @param {string} filePath The path to the configuration.
  250. * @returns {ConfigData|null} The configuration information.
  251. * @private
  252. */
  253. function loadConfigFile(filePath) {
  254. switch (path.extname(filePath)) {
  255. case ".js":
  256. case ".cjs":
  257. return loadJSConfigFile(filePath);
  258. case ".json":
  259. if (path.basename(filePath) === "package.json") {
  260. return loadPackageJSONConfigFile(filePath);
  261. }
  262. return loadJSONConfigFile(filePath);
  263. case ".yaml":
  264. case ".yml":
  265. return loadYAMLConfigFile(filePath);
  266. default:
  267. return loadLegacyConfigFile(filePath);
  268. }
  269. }
  270. /**
  271. * Write debug log.
  272. * @param {string} request The requested module name.
  273. * @param {string} relativeTo The file path to resolve the request relative to.
  274. * @param {string} filePath The resolved file path.
  275. * @returns {void}
  276. */
  277. function writeDebugLogForLoading(request, relativeTo, filePath) {
  278. /* istanbul ignore next */
  279. if (debug.enabled) {
  280. let nameAndVersion = null;
  281. try {
  282. const packageJsonPath = ModuleResolver.resolve(
  283. `${request}/package.json`,
  284. relativeTo
  285. );
  286. const { version = "unknown" } = require(packageJsonPath);
  287. nameAndVersion = `${request}@${version}`;
  288. } catch (error) {
  289. debug("package.json was not found:", error.message);
  290. nameAndVersion = request;
  291. }
  292. debug("Loaded: %s (%s)", nameAndVersion, filePath);
  293. }
  294. }
  295. /**
  296. * Concatenate two config data.
  297. * @param {IterableIterator<ConfigArrayElement>|null} elements The config elements.
  298. * @param {ConfigArray|null} parentConfigArray The parent config array.
  299. * @returns {ConfigArray} The concatenated config array.
  300. */
  301. function createConfigArray(elements, parentConfigArray) {
  302. if (!elements) {
  303. return parentConfigArray || new ConfigArray();
  304. }
  305. const configArray = new ConfigArray(...elements);
  306. if (parentConfigArray && !configArray.isRoot()) {
  307. configArray.unshift(...parentConfigArray);
  308. }
  309. return configArray;
  310. }
  311. /**
  312. * Normalize a given plugin.
  313. * - Ensure the object to have four properties: configs, environments, processors, and rules.
  314. * - Ensure the object to not have other properties.
  315. * @param {Plugin} plugin The plugin to normalize.
  316. * @returns {Plugin} The normalized plugin.
  317. */
  318. function normalizePlugin(plugin) {
  319. return {
  320. configs: plugin.configs || {},
  321. environments: plugin.environments || {},
  322. processors: plugin.processors || {},
  323. rules: plugin.rules || {}
  324. };
  325. }
  326. //------------------------------------------------------------------------------
  327. // Public Interface
  328. //------------------------------------------------------------------------------
  329. /**
  330. * The factory of `ConfigArray` objects.
  331. */
  332. class ConfigArrayFactory {
  333. /**
  334. * Initialize this instance.
  335. * @param {ConfigArrayFactoryOptions} [options] The map for additional plugins.
  336. */
  337. constructor({
  338. additionalPluginPool = new Map(),
  339. cwd = process.cwd(),
  340. resolvePluginsRelativeTo = cwd
  341. } = {}) {
  342. internalSlotsMap.set(this, { additionalPluginPool, cwd, resolvePluginsRelativeTo: path.resolve(cwd, resolvePluginsRelativeTo) });
  343. }
  344. /**
  345. * Create `ConfigArray` instance from a config data.
  346. * @param {ConfigData|null} configData The config data to create.
  347. * @param {Object} [options] The options.
  348. * @param {string} [options.filePath] The path to this config data.
  349. * @param {string} [options.name] The config name.
  350. * @param {ConfigArray} [options.parent] The parent config array.
  351. * @returns {ConfigArray} Loaded config.
  352. */
  353. create(configData, { filePath, name, parent } = {}) {
  354. return createConfigArray(
  355. configData
  356. ? this._normalizeConfigData(configData, filePath, name)
  357. : null,
  358. parent
  359. );
  360. }
  361. /**
  362. * Load a config file.
  363. * @param {string} filePath The path to a config file.
  364. * @param {Object} [options] The options.
  365. * @param {string} [options.name] The config name.
  366. * @param {ConfigArray} [options.parent] The parent config array.
  367. * @returns {ConfigArray} Loaded config.
  368. */
  369. loadFile(filePath, { name, parent } = {}) {
  370. const { cwd } = internalSlotsMap.get(this);
  371. const absolutePath = path.resolve(cwd, filePath);
  372. return createConfigArray(
  373. this._loadConfigData(absolutePath, name),
  374. parent
  375. );
  376. }
  377. /**
  378. * Load the config file on a given directory if exists.
  379. * @param {string} directoryPath The path to a directory.
  380. * @param {Object} [options] The options.
  381. * @param {string} [options.name] The config name.
  382. * @param {ConfigArray} [options.parent] The parent config array.
  383. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
  384. */
  385. loadInDirectory(directoryPath, { name, parent } = {}) {
  386. const { cwd } = internalSlotsMap.get(this);
  387. const absolutePath = path.resolve(cwd, directoryPath);
  388. return createConfigArray(
  389. this._loadConfigDataInDirectory(absolutePath, name),
  390. parent
  391. );
  392. }
  393. /**
  394. * Load `.eslintignore` file.
  395. * @param {string} filePath The path to a `.eslintignore` file to load.
  396. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
  397. */
  398. loadESLintIgnore(filePath) {
  399. const { cwd } = internalSlotsMap.get(this);
  400. const absolutePath = path.resolve(cwd, filePath);
  401. const name = path.relative(cwd, absolutePath);
  402. const ignorePatterns = loadESLintIgnoreFile(absolutePath);
  403. return createConfigArray(
  404. this._normalizeESLintIgnoreData(ignorePatterns, absolutePath, name)
  405. );
  406. }
  407. /**
  408. * Load `.eslintignore` file in the current working directory.
  409. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
  410. */
  411. loadDefaultESLintIgnore() {
  412. const { cwd } = internalSlotsMap.get(this);
  413. const eslintIgnorePath = path.resolve(cwd, ".eslintignore");
  414. const packageJsonPath = path.resolve(cwd, "package.json");
  415. if (fs.existsSync(eslintIgnorePath)) {
  416. return this.loadESLintIgnore(eslintIgnorePath);
  417. }
  418. if (fs.existsSync(packageJsonPath)) {
  419. const data = loadJSONConfigFile(packageJsonPath);
  420. if (Object.hasOwnProperty.call(data, "eslintIgnore")) {
  421. if (!Array.isArray(data.eslintIgnore)) {
  422. throw new Error("Package.json eslintIgnore property requires an array of paths");
  423. }
  424. return createConfigArray(
  425. this._normalizeESLintIgnoreData(
  426. data.eslintIgnore,
  427. packageJsonPath,
  428. "eslintIgnore in package.json"
  429. )
  430. );
  431. }
  432. }
  433. return new ConfigArray();
  434. }
  435. /**
  436. * Load a given config file.
  437. * @param {string} filePath The path to a config file.
  438. * @param {string} name The config name.
  439. * @returns {IterableIterator<ConfigArrayElement>} Loaded config.
  440. * @private
  441. */
  442. _loadConfigData(filePath, name) {
  443. return this._normalizeConfigData(
  444. loadConfigFile(filePath),
  445. filePath,
  446. name
  447. );
  448. }
  449. /**
  450. * Load the config file in a given directory if exists.
  451. * @param {string} directoryPath The path to a directory.
  452. * @param {string} name The config name.
  453. * @returns {IterableIterator<ConfigArrayElement> | null} Loaded config. `null` if any config doesn't exist.
  454. * @private
  455. */
  456. _loadConfigDataInDirectory(directoryPath, name) {
  457. for (const filename of configFilenames) {
  458. const filePath = path.join(directoryPath, filename);
  459. if (fs.existsSync(filePath)) {
  460. let configData;
  461. try {
  462. configData = loadConfigFile(filePath);
  463. } catch (error) {
  464. if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
  465. throw error;
  466. }
  467. }
  468. if (configData) {
  469. debug(`Config file found: ${filePath}`);
  470. return this._normalizeConfigData(configData, filePath, name);
  471. }
  472. }
  473. }
  474. debug(`Config file not found on ${directoryPath}`);
  475. return null;
  476. }
  477. /**
  478. * Normalize a given `.eslintignore` data to config array elements.
  479. * @param {string[]} ignorePatterns The patterns to ignore files.
  480. * @param {string|undefined} filePath The file path of this config.
  481. * @param {string|undefined} name The name of this config.
  482. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  483. * @private
  484. */
  485. *_normalizeESLintIgnoreData(ignorePatterns, filePath, name) {
  486. const elements = this._normalizeObjectConfigData(
  487. { ignorePatterns },
  488. filePath,
  489. name
  490. );
  491. // Set `ignorePattern.loose` flag for backward compatibility.
  492. for (const element of elements) {
  493. if (element.ignorePattern) {
  494. element.ignorePattern.loose = true;
  495. }
  496. yield element;
  497. }
  498. }
  499. /**
  500. * Normalize a given config to an array.
  501. * @param {ConfigData} configData The config data to normalize.
  502. * @param {string|undefined} providedFilePath The file path of this config.
  503. * @param {string|undefined} providedName The name of this config.
  504. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  505. * @private
  506. */
  507. _normalizeConfigData(configData, providedFilePath, providedName) {
  508. const { cwd } = internalSlotsMap.get(this);
  509. const filePath = providedFilePath
  510. ? path.resolve(cwd, providedFilePath)
  511. : "";
  512. const name = providedName || (filePath && path.relative(cwd, filePath));
  513. validateConfigSchema(configData, name || filePath);
  514. return this._normalizeObjectConfigData(configData, filePath, name);
  515. }
  516. /**
  517. * Normalize a given config to an array.
  518. * @param {ConfigData|OverrideConfigData} configData The config data to normalize.
  519. * @param {string} filePath The file path of this config.
  520. * @param {string} name The name of this config.
  521. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  522. * @private
  523. */
  524. *_normalizeObjectConfigData(configData, filePath, name) {
  525. const { cwd } = internalSlotsMap.get(this);
  526. const { files, excludedFiles, ...configBody } = configData;
  527. const basePath = filePath ? path.dirname(filePath) : cwd;
  528. const criteria = OverrideTester.create(files, excludedFiles, basePath);
  529. const elements =
  530. this._normalizeObjectConfigDataBody(configBody, filePath, name);
  531. // Apply the criteria to every element.
  532. for (const element of elements) {
  533. // Adopt the base path of the entry file (the outermost base path).
  534. if (element.criteria) {
  535. element.criteria.basePath = basePath;
  536. }
  537. if (element.ignorePattern) {
  538. element.ignorePattern.basePath = basePath;
  539. }
  540. /*
  541. * Merge the criteria; this is for only file extension processors in
  542. * `overrides` section for now.
  543. */
  544. element.criteria = OverrideTester.and(criteria, element.criteria);
  545. /*
  546. * Remove `root` property to ignore `root` settings which came from
  547. * `extends` in `overrides`.
  548. */
  549. if (element.criteria) {
  550. element.root = void 0;
  551. }
  552. yield element;
  553. }
  554. }
  555. /**
  556. * Normalize a given config to an array.
  557. * @param {ConfigData} configData The config data to normalize.
  558. * @param {string} filePath The file path of this config.
  559. * @param {string} name The name of this config.
  560. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  561. * @private
  562. */
  563. *_normalizeObjectConfigDataBody(
  564. {
  565. env,
  566. extends: extend,
  567. globals,
  568. ignorePatterns,
  569. noInlineConfig,
  570. parser: parserName,
  571. parserOptions,
  572. plugins: pluginList,
  573. processor,
  574. reportUnusedDisableDirectives,
  575. root,
  576. rules,
  577. settings,
  578. overrides: overrideList = []
  579. },
  580. filePath,
  581. name
  582. ) {
  583. const extendList = Array.isArray(extend) ? extend : [extend];
  584. const ignorePattern = ignorePatterns && new IgnorePattern(
  585. Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns],
  586. filePath ? path.dirname(filePath) : internalSlotsMap.get(this).cwd
  587. );
  588. // Flatten `extends`.
  589. for (const extendName of extendList.filter(Boolean)) {
  590. yield* this._loadExtends(extendName, filePath, name);
  591. }
  592. // Load parser & plugins.
  593. const parser =
  594. parserName && this._loadParser(parserName, filePath, name);
  595. const plugins =
  596. pluginList && this._loadPlugins(pluginList, filePath, name);
  597. // Yield pseudo config data for file extension processors.
  598. if (plugins) {
  599. yield* this._takeFileExtensionProcessors(plugins, filePath, name);
  600. }
  601. // Yield the config data except `extends` and `overrides`.
  602. yield {
  603. // Debug information.
  604. name,
  605. filePath,
  606. // Config data.
  607. criteria: null,
  608. env,
  609. globals,
  610. ignorePattern,
  611. noInlineConfig,
  612. parser,
  613. parserOptions,
  614. plugins,
  615. processor,
  616. reportUnusedDisableDirectives,
  617. root,
  618. rules,
  619. settings
  620. };
  621. // Flatten `overries`.
  622. for (let i = 0; i < overrideList.length; ++i) {
  623. yield* this._normalizeObjectConfigData(
  624. overrideList[i],
  625. filePath,
  626. `${name}#overrides[${i}]`
  627. );
  628. }
  629. }
  630. /**
  631. * Load configs of an element in `extends`.
  632. * @param {string} extendName The name of a base config.
  633. * @param {string} importerPath The file path which has the `extends` property.
  634. * @param {string} importerName The name of the config which has the `extends` property.
  635. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  636. * @private
  637. */
  638. _loadExtends(extendName, importerPath, importerName) {
  639. debug("Loading {extends:%j} relative to %s", extendName, importerPath);
  640. try {
  641. if (extendName.startsWith("eslint:")) {
  642. return this._loadExtendedBuiltInConfig(
  643. extendName,
  644. importerName
  645. );
  646. }
  647. if (extendName.startsWith("plugin:")) {
  648. return this._loadExtendedPluginConfig(
  649. extendName,
  650. importerPath,
  651. importerName
  652. );
  653. }
  654. return this._loadExtendedShareableConfig(
  655. extendName,
  656. importerPath,
  657. importerName
  658. );
  659. } catch (error) {
  660. error.message += `\nReferenced from: ${importerPath || importerName}`;
  661. throw error;
  662. }
  663. }
  664. /**
  665. * Load configs of an element in `extends`.
  666. * @param {string} extendName The name of a base config.
  667. * @param {string} importerName The name of the config which has the `extends` property.
  668. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  669. * @private
  670. */
  671. _loadExtendedBuiltInConfig(extendName, importerName) {
  672. const name = `${importerName} » ${extendName}`;
  673. if (extendName === "eslint:recommended") {
  674. return this._loadConfigData(eslintRecommendedPath, name);
  675. }
  676. if (extendName === "eslint:all") {
  677. return this._loadConfigData(eslintAllPath, name);
  678. }
  679. throw configMissingError(extendName, importerName);
  680. }
  681. /**
  682. * Load configs of an element in `extends`.
  683. * @param {string} extendName The name of a base config.
  684. * @param {string} importerPath The file path which has the `extends` property.
  685. * @param {string} importerName The name of the config which has the `extends` property.
  686. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  687. * @private
  688. */
  689. _loadExtendedPluginConfig(extendName, importerPath, importerName) {
  690. const slashIndex = extendName.lastIndexOf("/");
  691. const pluginName = extendName.slice("plugin:".length, slashIndex);
  692. const configName = extendName.slice(slashIndex + 1);
  693. if (isFilePath(pluginName)) {
  694. throw new Error("'extends' cannot use a file path for plugins.");
  695. }
  696. const plugin = this._loadPlugin(pluginName, importerPath, importerName);
  697. const configData =
  698. plugin.definition &&
  699. plugin.definition.configs[configName];
  700. if (configData) {
  701. return this._normalizeConfigData(
  702. configData,
  703. plugin.filePath,
  704. `${importerName} » plugin:${plugin.id}/${configName}`
  705. );
  706. }
  707. throw plugin.error || configMissingError(extendName, importerPath);
  708. }
  709. /**
  710. * Load configs of an element in `extends`.
  711. * @param {string} extendName The name of a base config.
  712. * @param {string} importerPath The file path which has the `extends` property.
  713. * @param {string} importerName The name of the config which has the `extends` property.
  714. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  715. * @private
  716. */
  717. _loadExtendedShareableConfig(extendName, importerPath, importerName) {
  718. const { cwd } = internalSlotsMap.get(this);
  719. const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
  720. let request;
  721. if (isFilePath(extendName)) {
  722. request = extendName;
  723. } else if (extendName.startsWith(".")) {
  724. request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
  725. } else {
  726. request = naming.normalizePackageName(
  727. extendName,
  728. "eslint-config"
  729. );
  730. }
  731. let filePath;
  732. try {
  733. filePath = ModuleResolver.resolve(request, relativeTo);
  734. } catch (error) {
  735. /* istanbul ignore else */
  736. if (error && error.code === "MODULE_NOT_FOUND") {
  737. throw configMissingError(extendName, importerPath);
  738. }
  739. throw error;
  740. }
  741. writeDebugLogForLoading(request, relativeTo, filePath);
  742. return this._loadConfigData(filePath, `${importerName} » ${request}`);
  743. }
  744. /**
  745. * Load given plugins.
  746. * @param {string[]} names The plugin names to load.
  747. * @param {string} importerPath The path to a config file that imports it. This is just a debug info.
  748. * @param {string} importerName The name of a config file that imports it. This is just a debug info.
  749. * @returns {Record<string,DependentPlugin>} The loaded parser.
  750. * @private
  751. */
  752. _loadPlugins(names, importerPath, importerName) {
  753. return names.reduce((map, name) => {
  754. if (isFilePath(name)) {
  755. throw new Error("Plugins array cannot includes file paths.");
  756. }
  757. const plugin = this._loadPlugin(name, importerPath, importerName);
  758. map[plugin.id] = plugin;
  759. return map;
  760. }, {});
  761. }
  762. /**
  763. * Load a given parser.
  764. * @param {string} nameOrPath The package name or the path to a parser file.
  765. * @param {string} importerPath The path to a config file that imports it.
  766. * @param {string} importerName The name of a config file that imports it. This is just a debug info.
  767. * @returns {DependentParser} The loaded parser.
  768. */
  769. _loadParser(nameOrPath, importerPath, importerName) {
  770. debug("Loading parser %j from %s", nameOrPath, importerPath);
  771. const { cwd } = internalSlotsMap.get(this);
  772. const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
  773. try {
  774. const filePath = ModuleResolver.resolve(nameOrPath, relativeTo);
  775. writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
  776. return new ConfigDependency({
  777. definition: require(filePath),
  778. filePath,
  779. id: nameOrPath,
  780. importerName,
  781. importerPath
  782. });
  783. } catch (error) {
  784. // If the parser name is "espree", load the espree of ESLint.
  785. if (nameOrPath === "espree") {
  786. debug("Fallback espree.");
  787. return new ConfigDependency({
  788. definition: require("espree"),
  789. filePath: require.resolve("espree"),
  790. id: nameOrPath,
  791. importerName,
  792. importerPath
  793. });
  794. }
  795. debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, importerName);
  796. error.message = `Failed to load parser '${nameOrPath}' declared in '${importerName}': ${error.message}`;
  797. return new ConfigDependency({
  798. error,
  799. id: nameOrPath,
  800. importerName,
  801. importerPath
  802. });
  803. }
  804. }
  805. /**
  806. * Load a given plugin.
  807. * @param {string} name The plugin name to load.
  808. * @param {string} importerPath The path to a config file that imports it. This is just a debug info.
  809. * @param {string} importerName The name of a config file that imports it. This is just a debug info.
  810. * @returns {DependentPlugin} The loaded plugin.
  811. * @private
  812. */
  813. _loadPlugin(name, importerPath, importerName) {
  814. debug("Loading plugin %j from %s", name, importerPath);
  815. const { additionalPluginPool, resolvePluginsRelativeTo } = internalSlotsMap.get(this);
  816. const request = naming.normalizePackageName(name, "eslint-plugin");
  817. const id = naming.getShorthandName(request, "eslint-plugin");
  818. const relativeTo = path.join(resolvePluginsRelativeTo, "__placeholder__.js");
  819. if (name.match(/\s+/u)) {
  820. const error = Object.assign(
  821. new Error(`Whitespace found in plugin name '${name}'`),
  822. {
  823. messageTemplate: "whitespace-found",
  824. messageData: { pluginName: request }
  825. }
  826. );
  827. return new ConfigDependency({
  828. error,
  829. id,
  830. importerName,
  831. importerPath
  832. });
  833. }
  834. // Check for additional pool.
  835. const plugin =
  836. additionalPluginPool.get(request) ||
  837. additionalPluginPool.get(id);
  838. if (plugin) {
  839. return new ConfigDependency({
  840. definition: normalizePlugin(plugin),
  841. filePath: importerPath,
  842. id,
  843. importerName,
  844. importerPath
  845. });
  846. }
  847. let filePath;
  848. let error;
  849. try {
  850. filePath = ModuleResolver.resolve(request, relativeTo);
  851. } catch (resolveError) {
  852. error = resolveError;
  853. /* istanbul ignore else */
  854. if (error && error.code === "MODULE_NOT_FOUND") {
  855. error.messageTemplate = "plugin-missing";
  856. error.messageData = {
  857. pluginName: request,
  858. resolvePluginsRelativeTo,
  859. importerName
  860. };
  861. }
  862. }
  863. if (filePath) {
  864. try {
  865. writeDebugLogForLoading(request, relativeTo, filePath);
  866. const startTime = Date.now();
  867. const pluginDefinition = require(filePath);
  868. debug(`Plugin ${filePath} loaded in: ${Date.now() - startTime}ms`);
  869. return new ConfigDependency({
  870. definition: normalizePlugin(pluginDefinition),
  871. filePath,
  872. id,
  873. importerName,
  874. importerPath
  875. });
  876. } catch (loadError) {
  877. error = loadError;
  878. }
  879. }
  880. debug("Failed to load plugin '%s' declared in '%s'.", name, importerName);
  881. error.message = `Failed to load plugin '${name}' declared in '${importerName}': ${error.message}`;
  882. return new ConfigDependency({
  883. error,
  884. id,
  885. importerName,
  886. importerPath
  887. });
  888. }
  889. /**
  890. * Take file expression processors as config array elements.
  891. * @param {Record<string,DependentPlugin>} plugins The plugin definitions.
  892. * @param {string} filePath The file path of this config.
  893. * @param {string} name The name of this config.
  894. * @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
  895. * @private
  896. */
  897. *_takeFileExtensionProcessors(plugins, filePath, name) {
  898. for (const pluginId of Object.keys(plugins)) {
  899. const processors =
  900. plugins[pluginId] &&
  901. plugins[pluginId].definition &&
  902. plugins[pluginId].definition.processors;
  903. if (!processors) {
  904. continue;
  905. }
  906. for (const processorId of Object.keys(processors)) {
  907. if (processorId.startsWith(".")) {
  908. yield* this._normalizeObjectConfigData(
  909. {
  910. files: [`*${processorId}`],
  911. processor: `${pluginId}/${processorId}`
  912. },
  913. filePath,
  914. `${name}#processors["${pluginId}/${processorId}"]`
  915. );
  916. }
  917. }
  918. }
  919. }
  920. }
  921. module.exports = { ConfigArrayFactory };