config-array-factory.js 40 KB

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