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.

1074 lines
37 KiB

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