config-array-factory.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080
  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 ConfigValidator = 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")("eslintrc:config-array-factory");
  54. //------------------------------------------------------------------------------
  55. // Helpers
  56. //------------------------------------------------------------------------------
  57. const eslintRecommendedPath = path.resolve(__dirname, "../../eslint/conf/eslint-recommended.js");
  58. const eslintAllPath = path.resolve(__dirname, "../../eslint/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. builtInRules,
  375. resolver = ModuleResolver
  376. } = {}) {
  377. internalSlotsMap.set(this, {
  378. additionalPluginPool,
  379. cwd,
  380. resolvePluginsRelativeTo:
  381. resolvePluginsRelativeTo &&
  382. path.resolve(cwd, resolvePluginsRelativeTo),
  383. builtInRules,
  384. resolver
  385. });
  386. }
  387. /**
  388. * Create `ConfigArray` instance from a config data.
  389. * @param {ConfigData|null} configData The config data to create.
  390. * @param {Object} [options] The options.
  391. * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
  392. * @param {string} [options.filePath] The path to this config data.
  393. * @param {string} [options.name] The config name.
  394. * @returns {ConfigArray} Loaded config.
  395. */
  396. create(configData, { basePath, filePath, name } = {}) {
  397. if (!configData) {
  398. return new ConfigArray();
  399. }
  400. const slots = internalSlotsMap.get(this);
  401. const ctx = createContext(slots, "config", name, filePath, basePath);
  402. const elements = this._normalizeConfigData(configData, ctx);
  403. return new ConfigArray(...elements);
  404. }
  405. /**
  406. * Load a config file.
  407. * @param {string} filePath The path to a config file.
  408. * @param {Object} [options] The options.
  409. * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
  410. * @param {string} [options.name] The config name.
  411. * @returns {ConfigArray} Loaded config.
  412. */
  413. loadFile(filePath, { basePath, name } = {}) {
  414. const slots = internalSlotsMap.get(this);
  415. const ctx = createContext(slots, "config", name, filePath, basePath);
  416. return new ConfigArray(...this._loadConfigData(ctx));
  417. }
  418. /**
  419. * Load the config file on a given directory if exists.
  420. * @param {string} directoryPath The path to a directory.
  421. * @param {Object} [options] The options.
  422. * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
  423. * @param {string} [options.name] The config name.
  424. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
  425. */
  426. loadInDirectory(directoryPath, { basePath, name } = {}) {
  427. const slots = internalSlotsMap.get(this);
  428. for (const filename of configFilenames) {
  429. const ctx = createContext(
  430. slots,
  431. "config",
  432. name,
  433. path.join(directoryPath, filename),
  434. basePath
  435. );
  436. if (fs.existsSync(ctx.filePath)) {
  437. let configData;
  438. try {
  439. configData = loadConfigFile(ctx.filePath);
  440. } catch (error) {
  441. if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
  442. throw error;
  443. }
  444. }
  445. if (configData) {
  446. debug(`Config file found: ${ctx.filePath}`);
  447. return new ConfigArray(
  448. ...this._normalizeConfigData(configData, ctx)
  449. );
  450. }
  451. }
  452. }
  453. debug(`Config file not found on ${directoryPath}`);
  454. return new ConfigArray();
  455. }
  456. /**
  457. * Check if a config file on a given directory exists or not.
  458. * @param {string} directoryPath The path to a directory.
  459. * @returns {string | null} The path to the found config file. If not found then null.
  460. */
  461. static getPathToConfigFileInDirectory(directoryPath) {
  462. for (const filename of configFilenames) {
  463. const filePath = path.join(directoryPath, filename);
  464. if (fs.existsSync(filePath)) {
  465. if (filename === "package.json") {
  466. try {
  467. loadPackageJSONConfigFile(filePath);
  468. return filePath;
  469. } catch { /* ignore */ }
  470. } else {
  471. return filePath;
  472. }
  473. }
  474. }
  475. return null;
  476. }
  477. /**
  478. * Load `.eslintignore` file.
  479. * @param {string} filePath The path to a `.eslintignore` file to load.
  480. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
  481. */
  482. loadESLintIgnore(filePath) {
  483. const slots = internalSlotsMap.get(this);
  484. const ctx = createContext(
  485. slots,
  486. "ignore",
  487. void 0,
  488. filePath,
  489. slots.cwd
  490. );
  491. const ignorePatterns = loadESLintIgnoreFile(ctx.filePath);
  492. return new ConfigArray(
  493. ...this._normalizeESLintIgnoreData(ignorePatterns, ctx)
  494. );
  495. }
  496. /**
  497. * Load `.eslintignore` file in the current working directory.
  498. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
  499. */
  500. loadDefaultESLintIgnore() {
  501. const slots = internalSlotsMap.get(this);
  502. const eslintIgnorePath = path.resolve(slots.cwd, ".eslintignore");
  503. const packageJsonPath = path.resolve(slots.cwd, "package.json");
  504. if (fs.existsSync(eslintIgnorePath)) {
  505. return this.loadESLintIgnore(eslintIgnorePath);
  506. }
  507. if (fs.existsSync(packageJsonPath)) {
  508. const data = loadJSONConfigFile(packageJsonPath);
  509. if (Object.hasOwnProperty.call(data, "eslintIgnore")) {
  510. if (!Array.isArray(data.eslintIgnore)) {
  511. throw new Error("Package.json eslintIgnore property requires an array of paths");
  512. }
  513. const ctx = createContext(
  514. slots,
  515. "ignore",
  516. "eslintIgnore in package.json",
  517. packageJsonPath,
  518. slots.cwd
  519. );
  520. return new ConfigArray(
  521. ...this._normalizeESLintIgnoreData(data.eslintIgnore, ctx)
  522. );
  523. }
  524. }
  525. return new ConfigArray();
  526. }
  527. /**
  528. * Load a given config file.
  529. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  530. * @returns {IterableIterator<ConfigArrayElement>} Loaded config.
  531. * @private
  532. */
  533. _loadConfigData(ctx) {
  534. return this._normalizeConfigData(loadConfigFile(ctx.filePath), ctx);
  535. }
  536. /**
  537. * Normalize a given `.eslintignore` data to config array elements.
  538. * @param {string[]} ignorePatterns The patterns to ignore files.
  539. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  540. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  541. * @private
  542. */
  543. *_normalizeESLintIgnoreData(ignorePatterns, ctx) {
  544. const elements = this._normalizeObjectConfigData(
  545. { ignorePatterns },
  546. ctx
  547. );
  548. // Set `ignorePattern.loose` flag for backward compatibility.
  549. for (const element of elements) {
  550. if (element.ignorePattern) {
  551. element.ignorePattern.loose = true;
  552. }
  553. yield element;
  554. }
  555. }
  556. /**
  557. * Normalize a given config to an array.
  558. * @param {ConfigData} configData The config data to normalize.
  559. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  560. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  561. * @private
  562. */
  563. _normalizeConfigData(configData, ctx) {
  564. const validator = new ConfigValidator();
  565. validator.validateConfigSchema(configData, ctx.name || ctx.filePath);
  566. return this._normalizeObjectConfigData(configData, ctx);
  567. }
  568. /**
  569. * Normalize a given config to an array.
  570. * @param {ConfigData|OverrideConfigData} configData The config data to normalize.
  571. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  572. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  573. * @private
  574. */
  575. *_normalizeObjectConfigData(configData, ctx) {
  576. const { files, excludedFiles, ...configBody } = configData;
  577. const criteria = OverrideTester.create(
  578. files,
  579. excludedFiles,
  580. ctx.matchBasePath
  581. );
  582. const elements = this._normalizeObjectConfigDataBody(configBody, ctx);
  583. // Apply the criteria to every element.
  584. for (const element of elements) {
  585. /*
  586. * Merge the criteria.
  587. * This is for the `overrides` entries that came from the
  588. * configurations of `overrides[].extends`.
  589. */
  590. element.criteria = OverrideTester.and(criteria, element.criteria);
  591. /*
  592. * Remove `root` property to ignore `root` settings which came from
  593. * `extends` in `overrides`.
  594. */
  595. if (element.criteria) {
  596. element.root = void 0;
  597. }
  598. yield element;
  599. }
  600. }
  601. /**
  602. * Normalize a given config to an array.
  603. * @param {ConfigData} configData The config data to normalize.
  604. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  605. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  606. * @private
  607. */
  608. *_normalizeObjectConfigDataBody(
  609. {
  610. env,
  611. extends: extend,
  612. globals,
  613. ignorePatterns,
  614. noInlineConfig,
  615. parser: parserName,
  616. parserOptions,
  617. plugins: pluginList,
  618. processor,
  619. reportUnusedDisableDirectives,
  620. root,
  621. rules,
  622. settings,
  623. overrides: overrideList = []
  624. },
  625. ctx
  626. ) {
  627. const extendList = Array.isArray(extend) ? extend : [extend];
  628. const ignorePattern = ignorePatterns && new IgnorePattern(
  629. Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns],
  630. ctx.matchBasePath
  631. );
  632. // Flatten `extends`.
  633. for (const extendName of extendList.filter(Boolean)) {
  634. yield* this._loadExtends(extendName, ctx);
  635. }
  636. // Load parser & plugins.
  637. const parser = parserName && this._loadParser(parserName, ctx);
  638. const plugins = pluginList && this._loadPlugins(pluginList, ctx);
  639. // Yield pseudo config data for file extension processors.
  640. if (plugins) {
  641. yield* this._takeFileExtensionProcessors(plugins, ctx);
  642. }
  643. // Yield the config data except `extends` and `overrides`.
  644. yield {
  645. // Debug information.
  646. type: ctx.type,
  647. name: ctx.name,
  648. filePath: ctx.filePath,
  649. // Config data.
  650. criteria: null,
  651. env,
  652. globals,
  653. ignorePattern,
  654. noInlineConfig,
  655. parser,
  656. parserOptions,
  657. plugins,
  658. processor,
  659. reportUnusedDisableDirectives,
  660. root,
  661. rules,
  662. settings
  663. };
  664. // Flatten `overries`.
  665. for (let i = 0; i < overrideList.length; ++i) {
  666. yield* this._normalizeObjectConfigData(
  667. overrideList[i],
  668. { ...ctx, name: `${ctx.name}#overrides[${i}]` }
  669. );
  670. }
  671. }
  672. /**
  673. * Load configs of an element in `extends`.
  674. * @param {string} extendName The name of a base config.
  675. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  676. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  677. * @private
  678. */
  679. _loadExtends(extendName, ctx) {
  680. debug("Loading {extends:%j} relative to %s", extendName, ctx.filePath);
  681. try {
  682. if (extendName.startsWith("eslint:")) {
  683. return this._loadExtendedBuiltInConfig(extendName, ctx);
  684. }
  685. if (extendName.startsWith("plugin:")) {
  686. return this._loadExtendedPluginConfig(extendName, ctx);
  687. }
  688. return this._loadExtendedShareableConfig(extendName, ctx);
  689. } catch (error) {
  690. error.message += `\nReferenced from: ${ctx.filePath || ctx.name}`;
  691. throw error;
  692. }
  693. }
  694. /**
  695. * Load configs of an element in `extends`.
  696. * @param {string} extendName The name of a base config.
  697. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  698. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  699. * @private
  700. */
  701. _loadExtendedBuiltInConfig(extendName, ctx) {
  702. if (extendName === "eslint:recommended") {
  703. return this._loadConfigData({
  704. ...ctx,
  705. filePath: eslintRecommendedPath,
  706. name: `${ctx.name} » ${extendName}`
  707. });
  708. }
  709. if (extendName === "eslint:all") {
  710. return this._loadConfigData({
  711. ...ctx,
  712. filePath: eslintAllPath,
  713. name: `${ctx.name} » ${extendName}`
  714. });
  715. }
  716. throw configMissingError(extendName, ctx.name);
  717. }
  718. /**
  719. * Load configs of an element in `extends`.
  720. * @param {string} extendName The name of a base config.
  721. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  722. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  723. * @private
  724. */
  725. _loadExtendedPluginConfig(extendName, ctx) {
  726. const slashIndex = extendName.lastIndexOf("/");
  727. const pluginName = extendName.slice("plugin:".length, slashIndex);
  728. const configName = extendName.slice(slashIndex + 1);
  729. if (isFilePath(pluginName)) {
  730. throw new Error("'extends' cannot use a file path for plugins.");
  731. }
  732. const plugin = this._loadPlugin(pluginName, ctx);
  733. const configData =
  734. plugin.definition &&
  735. plugin.definition.configs[configName];
  736. if (configData) {
  737. return this._normalizeConfigData(configData, {
  738. ...ctx,
  739. filePath: plugin.filePath || ctx.filePath,
  740. name: `${ctx.name} » plugin:${plugin.id}/${configName}`
  741. });
  742. }
  743. throw plugin.error || configMissingError(extendName, ctx.filePath);
  744. }
  745. /**
  746. * Load configs of an element in `extends`.
  747. * @param {string} extendName The name of a base config.
  748. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  749. * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
  750. * @private
  751. */
  752. _loadExtendedShareableConfig(extendName, ctx) {
  753. const { cwd, resolver } = internalSlotsMap.get(this);
  754. const relativeTo = ctx.filePath || path.join(cwd, "__placeholder__.js");
  755. let request;
  756. if (isFilePath(extendName)) {
  757. request = extendName;
  758. } else if (extendName.startsWith(".")) {
  759. request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
  760. } else {
  761. request = naming.normalizePackageName(
  762. extendName,
  763. "eslint-config"
  764. );
  765. }
  766. let filePath;
  767. try {
  768. filePath = resolver.resolve(request, relativeTo);
  769. } catch (error) {
  770. /* istanbul ignore else */
  771. if (error && error.code === "MODULE_NOT_FOUND") {
  772. throw configMissingError(extendName, ctx.filePath);
  773. }
  774. throw error;
  775. }
  776. writeDebugLogForLoading(request, relativeTo, filePath);
  777. return this._loadConfigData({
  778. ...ctx,
  779. filePath,
  780. name: `${ctx.name} » ${request}`
  781. });
  782. }
  783. /**
  784. * Load given plugins.
  785. * @param {string[]} names The plugin names to load.
  786. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  787. * @returns {Record<string,DependentPlugin>} The loaded parser.
  788. * @private
  789. */
  790. _loadPlugins(names, ctx) {
  791. return names.reduce((map, name) => {
  792. if (isFilePath(name)) {
  793. throw new Error("Plugins array cannot includes file paths.");
  794. }
  795. const plugin = this._loadPlugin(name, ctx);
  796. map[plugin.id] = plugin;
  797. return map;
  798. }, {});
  799. }
  800. /**
  801. * Load a given parser.
  802. * @param {string} nameOrPath The package name or the path to a parser file.
  803. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  804. * @returns {DependentParser} The loaded parser.
  805. */
  806. _loadParser(nameOrPath, ctx) {
  807. debug("Loading parser %j from %s", nameOrPath, ctx.filePath);
  808. const { cwd } = internalSlotsMap.get(this);
  809. const relativeTo = ctx.filePath || path.join(cwd, "__placeholder__.js");
  810. try {
  811. const filePath = ModuleResolver.resolve(nameOrPath, relativeTo);
  812. writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
  813. return new ConfigDependency({
  814. definition: require(filePath),
  815. filePath,
  816. id: nameOrPath,
  817. importerName: ctx.name,
  818. importerPath: ctx.filePath
  819. });
  820. } catch (error) {
  821. // If the parser name is "espree", load the espree of ESLint.
  822. if (nameOrPath === "espree") {
  823. debug("Fallback espree.");
  824. return new ConfigDependency({
  825. definition: require("espree"),
  826. filePath: require.resolve("espree"),
  827. id: nameOrPath,
  828. importerName: ctx.name,
  829. importerPath: ctx.filePath
  830. });
  831. }
  832. debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, ctx.name);
  833. error.message = `Failed to load parser '${nameOrPath}' declared in '${ctx.name}': ${error.message}`;
  834. return new ConfigDependency({
  835. error,
  836. id: nameOrPath,
  837. importerName: ctx.name,
  838. importerPath: ctx.filePath
  839. });
  840. }
  841. }
  842. /**
  843. * Load a given plugin.
  844. * @param {string} name The plugin name to load.
  845. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  846. * @returns {DependentPlugin} The loaded plugin.
  847. * @private
  848. */
  849. _loadPlugin(name, ctx) {
  850. debug("Loading plugin %j from %s", name, ctx.filePath);
  851. const { additionalPluginPool } = internalSlotsMap.get(this);
  852. const request = naming.normalizePackageName(name, "eslint-plugin");
  853. const id = naming.getShorthandName(request, "eslint-plugin");
  854. const relativeTo = path.join(ctx.pluginBasePath, "__placeholder__.js");
  855. if (name.match(/\s+/u)) {
  856. const error = Object.assign(
  857. new Error(`Whitespace found in plugin name '${name}'`),
  858. {
  859. messageTemplate: "whitespace-found",
  860. messageData: { pluginName: request }
  861. }
  862. );
  863. return new ConfigDependency({
  864. error,
  865. id,
  866. importerName: ctx.name,
  867. importerPath: ctx.filePath
  868. });
  869. }
  870. // Check for additional pool.
  871. const plugin =
  872. additionalPluginPool.get(request) ||
  873. additionalPluginPool.get(id);
  874. if (plugin) {
  875. return new ConfigDependency({
  876. definition: normalizePlugin(plugin),
  877. filePath: "", // It's unknown where the plugin came from.
  878. id,
  879. importerName: ctx.name,
  880. importerPath: ctx.filePath
  881. });
  882. }
  883. let filePath;
  884. let error;
  885. try {
  886. filePath = ModuleResolver.resolve(request, relativeTo);
  887. } catch (resolveError) {
  888. error = resolveError;
  889. /* istanbul ignore else */
  890. if (error && error.code === "MODULE_NOT_FOUND") {
  891. error.messageTemplate = "plugin-missing";
  892. error.messageData = {
  893. pluginName: request,
  894. resolvePluginsRelativeTo: ctx.pluginBasePath,
  895. importerName: ctx.name
  896. };
  897. }
  898. }
  899. if (filePath) {
  900. try {
  901. writeDebugLogForLoading(request, relativeTo, filePath);
  902. const startTime = Date.now();
  903. const pluginDefinition = require(filePath);
  904. debug(`Plugin ${filePath} loaded in: ${Date.now() - startTime}ms`);
  905. return new ConfigDependency({
  906. definition: normalizePlugin(pluginDefinition),
  907. filePath,
  908. id,
  909. importerName: ctx.name,
  910. importerPath: ctx.filePath
  911. });
  912. } catch (loadError) {
  913. error = loadError;
  914. }
  915. }
  916. debug("Failed to load plugin '%s' declared in '%s'.", name, ctx.name);
  917. error.message = `Failed to load plugin '${name}' declared in '${ctx.name}': ${error.message}`;
  918. return new ConfigDependency({
  919. error,
  920. id,
  921. importerName: ctx.name,
  922. importerPath: ctx.filePath
  923. });
  924. }
  925. /**
  926. * Take file expression processors as config array elements.
  927. * @param {Record<string,DependentPlugin>} plugins The plugin definitions.
  928. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
  929. * @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
  930. * @private
  931. */
  932. *_takeFileExtensionProcessors(plugins, ctx) {
  933. for (const pluginId of Object.keys(plugins)) {
  934. const processors =
  935. plugins[pluginId] &&
  936. plugins[pluginId].definition &&
  937. plugins[pluginId].definition.processors;
  938. if (!processors) {
  939. continue;
  940. }
  941. for (const processorId of Object.keys(processors)) {
  942. if (processorId.startsWith(".")) {
  943. yield* this._normalizeObjectConfigData(
  944. {
  945. files: [`*${processorId}`],
  946. processor: `${pluginId}/${processorId}`
  947. },
  948. {
  949. ...ctx,
  950. type: "implicit-processor",
  951. name: `${ctx.name}#processors["${pluginId}/${processorId}"]`
  952. }
  953. );
  954. }
  955. }
  956. }
  957. }
  958. }
  959. module.exports = { ConfigArrayFactory, createContext };