config-array-factory.js 38 KB

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