config-array-factory.js 40 KB

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