index.cjs 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. 'use strict';
  2. var path = require('node:path');
  3. var minimatch = require('minimatch');
  4. var createDebug = require('debug');
  5. var objectSchema = require('@eslint/object-schema');
  6. /**
  7. * @fileoverview ConfigSchema
  8. * @author Nicholas C. Zakas
  9. */
  10. //------------------------------------------------------------------------------
  11. // Types
  12. //------------------------------------------------------------------------------
  13. /** @typedef {import("@eslint/object-schema").PropertyDefinition} PropertyDefinition */
  14. /** @typedef {import("@eslint/object-schema").ObjectDefinition} ObjectDefinition */
  15. //------------------------------------------------------------------------------
  16. // Helpers
  17. //------------------------------------------------------------------------------
  18. /**
  19. * A strategy that does nothing.
  20. * @type {PropertyDefinition}
  21. */
  22. const NOOP_STRATEGY = {
  23. required: false,
  24. merge() {
  25. return undefined;
  26. },
  27. validate() {},
  28. };
  29. //------------------------------------------------------------------------------
  30. // Exports
  31. //------------------------------------------------------------------------------
  32. /**
  33. * The base schema that every ConfigArray uses.
  34. * @type {ObjectDefinition}
  35. */
  36. const baseSchema = Object.freeze({
  37. name: {
  38. required: false,
  39. merge() {
  40. return undefined;
  41. },
  42. validate(value) {
  43. if (typeof value !== "string") {
  44. throw new TypeError("Property must be a string.");
  45. }
  46. },
  47. },
  48. files: NOOP_STRATEGY,
  49. ignores: NOOP_STRATEGY,
  50. });
  51. /**
  52. * @fileoverview ConfigSchema
  53. * @author Nicholas C. Zakas
  54. */
  55. //------------------------------------------------------------------------------
  56. // Types
  57. //------------------------------------------------------------------------------
  58. //------------------------------------------------------------------------------
  59. // Helpers
  60. //------------------------------------------------------------------------------
  61. /**
  62. * Asserts that a given value is an array.
  63. * @param {*} value The value to check.
  64. * @returns {void}
  65. * @throws {TypeError} When the value is not an array.
  66. */
  67. function assertIsArray(value) {
  68. if (!Array.isArray(value)) {
  69. throw new TypeError("Expected value to be an array.");
  70. }
  71. }
  72. /**
  73. * Asserts that a given value is an array containing only strings and functions.
  74. * @param {*} value The value to check.
  75. * @returns {void}
  76. * @throws {TypeError} When the value is not an array of strings and functions.
  77. */
  78. function assertIsArrayOfStringsAndFunctions(value) {
  79. assertIsArray(value);
  80. if (
  81. value.some(
  82. item => typeof item !== "string" && typeof item !== "function",
  83. )
  84. ) {
  85. throw new TypeError(
  86. "Expected array to only contain strings and functions.",
  87. );
  88. }
  89. }
  90. /**
  91. * Asserts that a given value is a non-empty array.
  92. * @param {*} value The value to check.
  93. * @returns {void}
  94. * @throws {TypeError} When the value is not an array or an empty array.
  95. */
  96. function assertIsNonEmptyArray(value) {
  97. if (!Array.isArray(value) || value.length === 0) {
  98. throw new TypeError("Expected value to be a non-empty array.");
  99. }
  100. }
  101. //------------------------------------------------------------------------------
  102. // Exports
  103. //------------------------------------------------------------------------------
  104. /**
  105. * The schema for `files` and `ignores` that every ConfigArray uses.
  106. * @type {ObjectDefinition}
  107. */
  108. const filesAndIgnoresSchema = Object.freeze({
  109. files: {
  110. required: false,
  111. merge() {
  112. return undefined;
  113. },
  114. validate(value) {
  115. // first check if it's an array
  116. assertIsNonEmptyArray(value);
  117. // then check each member
  118. value.forEach(item => {
  119. if (Array.isArray(item)) {
  120. assertIsArrayOfStringsAndFunctions(item);
  121. } else if (
  122. typeof item !== "string" &&
  123. typeof item !== "function"
  124. ) {
  125. throw new TypeError(
  126. "Items must be a string, a function, or an array of strings and functions.",
  127. );
  128. }
  129. });
  130. },
  131. },
  132. ignores: {
  133. required: false,
  134. merge() {
  135. return undefined;
  136. },
  137. validate: assertIsArrayOfStringsAndFunctions,
  138. },
  139. });
  140. /**
  141. * @fileoverview ConfigArray
  142. * @author Nicholas C. Zakas
  143. */
  144. //------------------------------------------------------------------------------
  145. // Types
  146. //------------------------------------------------------------------------------
  147. /** @typedef {import("./types.ts").ConfigObject} ConfigObject */
  148. /** @typedef {import("minimatch").IMinimatchStatic} IMinimatchStatic */
  149. /** @typedef {import("minimatch").IMinimatch} IMinimatch */
  150. /*
  151. * This is a bit of a hack to make TypeScript happy with the Rollup-created
  152. * CommonJS file. Rollup doesn't do object destructuring for imported files
  153. * and instead imports the default via `require()`. This messes up type checking
  154. * for `ObjectSchema`. To work around that, we just import the type manually
  155. * and give it a different name to use in the JSDoc comments.
  156. */
  157. /** @typedef {import("@eslint/object-schema").ObjectSchema} ObjectSchemaInstance */
  158. //------------------------------------------------------------------------------
  159. // Helpers
  160. //------------------------------------------------------------------------------
  161. const Minimatch = minimatch.Minimatch;
  162. const debug = createDebug("@eslint/config-array");
  163. /**
  164. * A cache for minimatch instances.
  165. * @type {Map<string, IMinimatch>}
  166. */
  167. const minimatchCache = new Map();
  168. /**
  169. * A cache for negated minimatch instances.
  170. * @type {Map<string, IMinimatch>}
  171. */
  172. const negatedMinimatchCache = new Map();
  173. /**
  174. * Options to use with minimatch.
  175. * @type {Object}
  176. */
  177. const MINIMATCH_OPTIONS = {
  178. // matchBase: true,
  179. dot: true,
  180. allowWindowsEscape: true,
  181. };
  182. /**
  183. * The types of config objects that are supported.
  184. * @type {Set<string>}
  185. */
  186. const CONFIG_TYPES = new Set(["array", "function"]);
  187. /**
  188. * Fields that are considered metadata and not part of the config object.
  189. * @type {Set<string>}
  190. */
  191. const META_FIELDS = new Set(["name"]);
  192. /**
  193. * A schema containing just files and ignores for early validation.
  194. * @type {ObjectSchemaInstance}
  195. */
  196. const FILES_AND_IGNORES_SCHEMA = new objectSchema.ObjectSchema(filesAndIgnoresSchema);
  197. // Precomputed constant objects returned by `ConfigArray.getConfigWithStatus`.
  198. const CONFIG_WITH_STATUS_EXTERNAL = Object.freeze({ status: "external" });
  199. const CONFIG_WITH_STATUS_IGNORED = Object.freeze({ status: "ignored" });
  200. const CONFIG_WITH_STATUS_UNCONFIGURED = Object.freeze({
  201. status: "unconfigured",
  202. });
  203. /**
  204. * Wrapper error for config validation errors that adds a name to the front of the
  205. * error message.
  206. */
  207. class ConfigError extends Error {
  208. /**
  209. * Creates a new instance.
  210. * @param {string} name The config object name causing the error.
  211. * @param {number} index The index of the config object in the array.
  212. * @param {Object} options The options for the error.
  213. * @param {Error} [options.cause] The error that caused this error.
  214. * @param {string} [options.message] The message to use for the error.
  215. */
  216. constructor(name, index, { cause, message }) {
  217. const finalMessage = message || cause.message;
  218. super(`Config ${name}: ${finalMessage}`, { cause });
  219. // copy over custom properties that aren't represented
  220. if (cause) {
  221. for (const key of Object.keys(cause)) {
  222. if (!(key in this)) {
  223. this[key] = cause[key];
  224. }
  225. }
  226. }
  227. /**
  228. * The name of the error.
  229. * @type {string}
  230. * @readonly
  231. */
  232. this.name = "ConfigError";
  233. /**
  234. * The index of the config object in the array.
  235. * @type {number}
  236. * @readonly
  237. */
  238. this.index = index;
  239. }
  240. }
  241. /**
  242. * Gets the name of a config object.
  243. * @param {ConfigObject} config The config object to get the name of.
  244. * @returns {string} The name of the config object.
  245. */
  246. function getConfigName(config) {
  247. if (config && typeof config.name === "string" && config.name) {
  248. return `"${config.name}"`;
  249. }
  250. return "(unnamed)";
  251. }
  252. /**
  253. * Rethrows a config error with additional information about the config object.
  254. * @param {object} config The config object to get the name of.
  255. * @param {number} index The index of the config object in the array.
  256. * @param {Error} error The error to rethrow.
  257. * @throws {ConfigError} When the error is rethrown for a config.
  258. */
  259. function rethrowConfigError(config, index, error) {
  260. const configName = getConfigName(config);
  261. throw new ConfigError(configName, index, { cause: error });
  262. }
  263. /**
  264. * Shorthand for checking if a value is a string.
  265. * @param {any} value The value to check.
  266. * @returns {boolean} True if a string, false if not.
  267. */
  268. function isString(value) {
  269. return typeof value === "string";
  270. }
  271. /**
  272. * Creates a function that asserts that the config is valid
  273. * during normalization. This checks that the config is not nullish
  274. * and that files and ignores keys of a config object are valid as per base schema.
  275. * @param {Object} config The config object to check.
  276. * @param {number} index The index of the config object in the array.
  277. * @returns {void}
  278. * @throws {ConfigError} If the files and ignores keys of a config object are not valid.
  279. */
  280. function assertValidBaseConfig(config, index) {
  281. if (config === null) {
  282. throw new ConfigError(getConfigName(config), index, {
  283. message: "Unexpected null config.",
  284. });
  285. }
  286. if (config === undefined) {
  287. throw new ConfigError(getConfigName(config), index, {
  288. message: "Unexpected undefined config.",
  289. });
  290. }
  291. if (typeof config !== "object") {
  292. throw new ConfigError(getConfigName(config), index, {
  293. message: "Unexpected non-object config.",
  294. });
  295. }
  296. const validateConfig = {};
  297. if ("files" in config) {
  298. validateConfig.files = config.files;
  299. }
  300. if ("ignores" in config) {
  301. validateConfig.ignores = config.ignores;
  302. }
  303. try {
  304. FILES_AND_IGNORES_SCHEMA.validate(validateConfig);
  305. } catch (validationError) {
  306. rethrowConfigError(config, index, validationError);
  307. }
  308. }
  309. /**
  310. * Wrapper around minimatch that caches minimatch patterns for
  311. * faster matching speed over multiple file path evaluations.
  312. * @param {string} filepath The file path to match.
  313. * @param {string} pattern The glob pattern to match against.
  314. * @param {object} options The minimatch options to use.
  315. * @returns
  316. */
  317. function doMatch(filepath, pattern, options = {}) {
  318. let cache = minimatchCache;
  319. if (options.flipNegate) {
  320. cache = negatedMinimatchCache;
  321. }
  322. let matcher = cache.get(pattern);
  323. if (!matcher) {
  324. matcher = new Minimatch(
  325. pattern,
  326. Object.assign({}, MINIMATCH_OPTIONS, options),
  327. );
  328. cache.set(pattern, matcher);
  329. }
  330. return matcher.match(filepath);
  331. }
  332. /**
  333. * Normalizes a `ConfigArray` by flattening it and executing any functions
  334. * that are found inside.
  335. * @param {Array} items The items in a `ConfigArray`.
  336. * @param {Object} context The context object to pass into any function
  337. * found.
  338. * @param {Array<string>} extraConfigTypes The config types to check.
  339. * @returns {Promise<Array>} A flattened array containing only config objects.
  340. * @throws {TypeError} When a config function returns a function.
  341. */
  342. async function normalize(items, context, extraConfigTypes) {
  343. const allowFunctions = extraConfigTypes.includes("function");
  344. const allowArrays = extraConfigTypes.includes("array");
  345. async function* flatTraverse(array) {
  346. for (let item of array) {
  347. if (typeof item === "function") {
  348. if (!allowFunctions) {
  349. throw new TypeError("Unexpected function.");
  350. }
  351. item = item(context);
  352. if (item.then) {
  353. item = await item;
  354. }
  355. }
  356. if (Array.isArray(item)) {
  357. if (!allowArrays) {
  358. throw new TypeError("Unexpected array.");
  359. }
  360. yield* flatTraverse(item);
  361. } else if (typeof item === "function") {
  362. throw new TypeError(
  363. "A config function can only return an object or array.",
  364. );
  365. } else {
  366. yield item;
  367. }
  368. }
  369. }
  370. /*
  371. * Async iterables cannot be used with the spread operator, so we need to manually
  372. * create the array to return.
  373. */
  374. const asyncIterable = await flatTraverse(items);
  375. const configs = [];
  376. for await (const config of asyncIterable) {
  377. configs.push(config);
  378. }
  379. return configs;
  380. }
  381. /**
  382. * Normalizes a `ConfigArray` by flattening it and executing any functions
  383. * that are found inside.
  384. * @param {Array} items The items in a `ConfigArray`.
  385. * @param {Object} context The context object to pass into any function
  386. * found.
  387. * @param {Array<string>} extraConfigTypes The config types to check.
  388. * @returns {Array} A flattened array containing only config objects.
  389. * @throws {TypeError} When a config function returns a function.
  390. */
  391. function normalizeSync(items, context, extraConfigTypes) {
  392. const allowFunctions = extraConfigTypes.includes("function");
  393. const allowArrays = extraConfigTypes.includes("array");
  394. function* flatTraverse(array) {
  395. for (let item of array) {
  396. if (typeof item === "function") {
  397. if (!allowFunctions) {
  398. throw new TypeError("Unexpected function.");
  399. }
  400. item = item(context);
  401. if (item.then) {
  402. throw new TypeError(
  403. "Async config functions are not supported.",
  404. );
  405. }
  406. }
  407. if (Array.isArray(item)) {
  408. if (!allowArrays) {
  409. throw new TypeError("Unexpected array.");
  410. }
  411. yield* flatTraverse(item);
  412. } else if (typeof item === "function") {
  413. throw new TypeError(
  414. "A config function can only return an object or array.",
  415. );
  416. } else {
  417. yield item;
  418. }
  419. }
  420. }
  421. return [...flatTraverse(items)];
  422. }
  423. /**
  424. * Determines if a given file path should be ignored based on the given
  425. * matcher.
  426. * @param {Array<string|((string) => boolean)>} ignores The ignore patterns to check.
  427. * @param {string} filePath The absolute path of the file to check.
  428. * @param {string} relativeFilePath The relative path of the file to check.
  429. * @returns {boolean} True if the path should be ignored and false if not.
  430. */
  431. function shouldIgnorePath(ignores, filePath, relativeFilePath) {
  432. // all files outside of the basePath are ignored
  433. if (relativeFilePath.startsWith("..")) {
  434. return true;
  435. }
  436. return ignores.reduce((ignored, matcher) => {
  437. if (!ignored) {
  438. if (typeof matcher === "function") {
  439. return matcher(filePath);
  440. }
  441. // don't check negated patterns because we're not ignored yet
  442. if (!matcher.startsWith("!")) {
  443. return doMatch(relativeFilePath, matcher);
  444. }
  445. // otherwise we're still not ignored
  446. return false;
  447. }
  448. // only need to check negated patterns because we're ignored
  449. if (typeof matcher === "string" && matcher.startsWith("!")) {
  450. return !doMatch(relativeFilePath, matcher, {
  451. flipNegate: true,
  452. });
  453. }
  454. return ignored;
  455. }, false);
  456. }
  457. /**
  458. * Determines if a given file path is matched by a config based on
  459. * `ignores` only.
  460. * @param {string} filePath The absolute file path to check.
  461. * @param {string} basePath The base path for the config.
  462. * @param {Object} config The config object to check.
  463. * @returns {boolean} True if the file path is matched by the config,
  464. * false if not.
  465. */
  466. function pathMatchesIgnores(filePath, basePath, config) {
  467. /*
  468. * For both files and ignores, functions are passed the absolute
  469. * file path while strings are compared against the relative
  470. * file path.
  471. */
  472. const relativeFilePath = path.relative(basePath, filePath);
  473. return (
  474. Object.keys(config).filter(key => !META_FIELDS.has(key)).length > 1 &&
  475. !shouldIgnorePath(config.ignores, filePath, relativeFilePath)
  476. );
  477. }
  478. /**
  479. * Determines if a given file path is matched by a config. If the config
  480. * has no `files` field, then it matches; otherwise, if a `files` field
  481. * is present then we match the globs in `files` and exclude any globs in
  482. * `ignores`.
  483. * @param {string} filePath The absolute file path to check.
  484. * @param {string} basePath The base path for the config.
  485. * @param {Object} config The config object to check.
  486. * @returns {boolean} True if the file path is matched by the config,
  487. * false if not.
  488. */
  489. function pathMatches(filePath, basePath, config) {
  490. /*
  491. * For both files and ignores, functions are passed the absolute
  492. * file path while strings are compared against the relative
  493. * file path.
  494. */
  495. const relativeFilePath = path.relative(basePath, filePath);
  496. // match both strings and functions
  497. function match(pattern) {
  498. if (isString(pattern)) {
  499. return doMatch(relativeFilePath, pattern);
  500. }
  501. if (typeof pattern === "function") {
  502. return pattern(filePath);
  503. }
  504. throw new TypeError(`Unexpected matcher type ${pattern}.`);
  505. }
  506. // check for all matches to config.files
  507. let filePathMatchesPattern = config.files.some(pattern => {
  508. if (Array.isArray(pattern)) {
  509. return pattern.every(match);
  510. }
  511. return match(pattern);
  512. });
  513. /*
  514. * If the file path matches the config.files patterns, then check to see
  515. * if there are any files to ignore.
  516. */
  517. if (filePathMatchesPattern && config.ignores) {
  518. filePathMatchesPattern = !shouldIgnorePath(
  519. config.ignores,
  520. filePath,
  521. relativeFilePath,
  522. );
  523. }
  524. return filePathMatchesPattern;
  525. }
  526. /**
  527. * Ensures that a ConfigArray has been normalized.
  528. * @param {ConfigArray} configArray The ConfigArray to check.
  529. * @returns {void}
  530. * @throws {Error} When the `ConfigArray` is not normalized.
  531. */
  532. function assertNormalized(configArray) {
  533. // TODO: Throw more verbose error
  534. if (!configArray.isNormalized()) {
  535. throw new Error(
  536. "ConfigArray must be normalized to perform this operation.",
  537. );
  538. }
  539. }
  540. /**
  541. * Ensures that config types are valid.
  542. * @param {Array<string>} extraConfigTypes The config types to check.
  543. * @returns {void}
  544. * @throws {Error} When the config types array is invalid.
  545. */
  546. function assertExtraConfigTypes(extraConfigTypes) {
  547. if (extraConfigTypes.length > 2) {
  548. throw new TypeError(
  549. "configTypes must be an array with at most two items.",
  550. );
  551. }
  552. for (const configType of extraConfigTypes) {
  553. if (!CONFIG_TYPES.has(configType)) {
  554. throw new TypeError(
  555. `Unexpected config type "${configType}" found. Expected one of: "object", "array", "function".`,
  556. );
  557. }
  558. }
  559. }
  560. //------------------------------------------------------------------------------
  561. // Public Interface
  562. //------------------------------------------------------------------------------
  563. const ConfigArraySymbol = {
  564. isNormalized: Symbol("isNormalized"),
  565. configCache: Symbol("configCache"),
  566. schema: Symbol("schema"),
  567. finalizeConfig: Symbol("finalizeConfig"),
  568. preprocessConfig: Symbol("preprocessConfig"),
  569. };
  570. // used to store calculate data for faster lookup
  571. const dataCache = new WeakMap();
  572. /**
  573. * Represents an array of config objects and provides method for working with
  574. * those config objects.
  575. */
  576. class ConfigArray extends Array {
  577. /**
  578. * Creates a new instance of ConfigArray.
  579. * @param {Iterable|Function|Object} configs An iterable yielding config
  580. * objects, or a config function, or a config object.
  581. * @param {Object} options The options for the ConfigArray.
  582. * @param {string} [options.basePath=""] The path of the config file
  583. * @param {boolean} [options.normalized=false] Flag indicating if the
  584. * configs have already been normalized.
  585. * @param {Object} [options.schema] The additional schema
  586. * definitions to use for the ConfigArray schema.
  587. * @param {Array<string>} [options.extraConfigTypes] List of config types supported.
  588. */
  589. constructor(
  590. configs,
  591. {
  592. basePath = "",
  593. normalized = false,
  594. schema: customSchema,
  595. extraConfigTypes = [],
  596. } = {},
  597. ) {
  598. super();
  599. /**
  600. * Tracks if the array has been normalized.
  601. * @property isNormalized
  602. * @type {boolean}
  603. * @private
  604. */
  605. this[ConfigArraySymbol.isNormalized] = normalized;
  606. /**
  607. * The schema used for validating and merging configs.
  608. * @property schema
  609. * @type {ObjectSchemaInstance}
  610. * @private
  611. */
  612. this[ConfigArraySymbol.schema] = new objectSchema.ObjectSchema(
  613. Object.assign({}, customSchema, baseSchema),
  614. );
  615. /**
  616. * The path of the config file that this array was loaded from.
  617. * This is used to calculate filename matches.
  618. * @property basePath
  619. * @type {string}
  620. */
  621. this.basePath = basePath;
  622. assertExtraConfigTypes(extraConfigTypes);
  623. /**
  624. * The supported config types.
  625. * @type {Array<string>}
  626. */
  627. this.extraConfigTypes = [...extraConfigTypes];
  628. Object.freeze(this.extraConfigTypes);
  629. /**
  630. * A cache to store calculated configs for faster repeat lookup.
  631. * @property configCache
  632. * @type {Map<string, Object>}
  633. * @private
  634. */
  635. this[ConfigArraySymbol.configCache] = new Map();
  636. // init cache
  637. dataCache.set(this, {
  638. explicitMatches: new Map(),
  639. directoryMatches: new Map(),
  640. files: undefined,
  641. ignores: undefined,
  642. });
  643. // load the configs into this array
  644. if (Array.isArray(configs)) {
  645. this.push(...configs);
  646. } else {
  647. this.push(configs);
  648. }
  649. }
  650. /**
  651. * Prevent normal array methods from creating a new `ConfigArray` instance.
  652. * This is to ensure that methods such as `slice()` won't try to create a
  653. * new instance of `ConfigArray` behind the scenes as doing so may throw
  654. * an error due to the different constructor signature.
  655. * @type {ArrayConstructor} The `Array` constructor.
  656. */
  657. static get [Symbol.species]() {
  658. return Array;
  659. }
  660. /**
  661. * Returns the `files` globs from every config object in the array.
  662. * This can be used to determine which files will be matched by a
  663. * config array or to use as a glob pattern when no patterns are provided
  664. * for a command line interface.
  665. * @returns {Array<string|Function>} An array of matchers.
  666. */
  667. get files() {
  668. assertNormalized(this);
  669. // if this data has been cached, retrieve it
  670. const cache = dataCache.get(this);
  671. if (cache.files) {
  672. return cache.files;
  673. }
  674. // otherwise calculate it
  675. const result = [];
  676. for (const config of this) {
  677. if (config.files) {
  678. config.files.forEach(filePattern => {
  679. result.push(filePattern);
  680. });
  681. }
  682. }
  683. // store result
  684. cache.files = result;
  685. dataCache.set(this, cache);
  686. return result;
  687. }
  688. /**
  689. * Returns ignore matchers that should always be ignored regardless of
  690. * the matching `files` fields in any configs. This is necessary to mimic
  691. * the behavior of things like .gitignore and .eslintignore, allowing a
  692. * globbing operation to be faster.
  693. * @returns {string[]} An array of string patterns and functions to be ignored.
  694. */
  695. get ignores() {
  696. assertNormalized(this);
  697. // if this data has been cached, retrieve it
  698. const cache = dataCache.get(this);
  699. if (cache.ignores) {
  700. return cache.ignores;
  701. }
  702. // otherwise calculate it
  703. const result = [];
  704. for (const config of this) {
  705. /*
  706. * We only count ignores if there are no other keys in the object.
  707. * In this case, it acts list a globally ignored pattern. If there
  708. * are additional keys, then ignores act like exclusions.
  709. */
  710. if (
  711. config.ignores &&
  712. Object.keys(config).filter(key => !META_FIELDS.has(key))
  713. .length === 1
  714. ) {
  715. result.push(...config.ignores);
  716. }
  717. }
  718. // store result
  719. cache.ignores = result;
  720. dataCache.set(this, cache);
  721. return result;
  722. }
  723. /**
  724. * Indicates if the config array has been normalized.
  725. * @returns {boolean} True if the config array is normalized, false if not.
  726. */
  727. isNormalized() {
  728. return this[ConfigArraySymbol.isNormalized];
  729. }
  730. /**
  731. * Normalizes a config array by flattening embedded arrays and executing
  732. * config functions.
  733. * @param {Object} [context] The context object for config functions.
  734. * @returns {Promise<ConfigArray>} The current ConfigArray instance.
  735. */
  736. async normalize(context = {}) {
  737. if (!this.isNormalized()) {
  738. const normalizedConfigs = await normalize(
  739. this,
  740. context,
  741. this.extraConfigTypes,
  742. );
  743. this.length = 0;
  744. this.push(
  745. ...normalizedConfigs.map(
  746. this[ConfigArraySymbol.preprocessConfig].bind(this),
  747. ),
  748. );
  749. this.forEach(assertValidBaseConfig);
  750. this[ConfigArraySymbol.isNormalized] = true;
  751. // prevent further changes
  752. Object.freeze(this);
  753. }
  754. return this;
  755. }
  756. /**
  757. * Normalizes a config array by flattening embedded arrays and executing
  758. * config functions.
  759. * @param {Object} [context] The context object for config functions.
  760. * @returns {ConfigArray} The current ConfigArray instance.
  761. */
  762. normalizeSync(context = {}) {
  763. if (!this.isNormalized()) {
  764. const normalizedConfigs = normalizeSync(
  765. this,
  766. context,
  767. this.extraConfigTypes,
  768. );
  769. this.length = 0;
  770. this.push(
  771. ...normalizedConfigs.map(
  772. this[ConfigArraySymbol.preprocessConfig].bind(this),
  773. ),
  774. );
  775. this.forEach(assertValidBaseConfig);
  776. this[ConfigArraySymbol.isNormalized] = true;
  777. // prevent further changes
  778. Object.freeze(this);
  779. }
  780. return this;
  781. }
  782. /* eslint-disable class-methods-use-this -- Desired as instance methods */
  783. /**
  784. * Finalizes the state of a config before being cached and returned by
  785. * `getConfig()`. Does nothing by default but is provided to be
  786. * overridden by subclasses as necessary.
  787. * @param {Object} config The config to finalize.
  788. * @returns {Object} The finalized config.
  789. */
  790. [ConfigArraySymbol.finalizeConfig](config) {
  791. return config;
  792. }
  793. /**
  794. * Preprocesses a config during the normalization process. This is the
  795. * method to override if you want to convert an array item before it is
  796. * validated for the first time. For example, if you want to replace a
  797. * string with an object, this is the method to override.
  798. * @param {Object} config The config to preprocess.
  799. * @returns {Object} The config to use in place of the argument.
  800. */
  801. [ConfigArraySymbol.preprocessConfig](config) {
  802. return config;
  803. }
  804. /* eslint-enable class-methods-use-this -- Desired as instance methods */
  805. /**
  806. * Returns the config object for a given file path and a status that can be used to determine why a file has no config.
  807. * @param {string} filePath The complete path of a file to get a config for.
  808. * @returns {{ config?: Object, status: "ignored"|"external"|"unconfigured"|"matched" }}
  809. * An object with an optional property `config` and property `status`.
  810. * `config` is the config object for the specified file as returned by {@linkcode ConfigArray.getConfig},
  811. * `status` a is one of the constants returned by {@linkcode ConfigArray.getConfigStatus}.
  812. */
  813. getConfigWithStatus(filePath) {
  814. assertNormalized(this);
  815. const cache = this[ConfigArraySymbol.configCache];
  816. // first check the cache for a filename match to avoid duplicate work
  817. if (cache.has(filePath)) {
  818. return cache.get(filePath);
  819. }
  820. // check to see if the file is outside the base path
  821. const relativeFilePath = path.relative(this.basePath, filePath);
  822. if (relativeFilePath.startsWith("..")) {
  823. debug(`No config for file ${filePath} outside of base path`);
  824. // cache and return result
  825. cache.set(filePath, CONFIG_WITH_STATUS_EXTERNAL);
  826. return CONFIG_WITH_STATUS_EXTERNAL;
  827. }
  828. // next check to see if the file should be ignored
  829. // check if this should be ignored due to its directory
  830. if (this.isDirectoryIgnored(path.dirname(filePath))) {
  831. debug(`Ignoring ${filePath} based on directory pattern`);
  832. // cache and return result
  833. cache.set(filePath, CONFIG_WITH_STATUS_IGNORED);
  834. return CONFIG_WITH_STATUS_IGNORED;
  835. }
  836. if (shouldIgnorePath(this.ignores, filePath, relativeFilePath)) {
  837. debug(`Ignoring ${filePath} based on file pattern`);
  838. // cache and return result
  839. cache.set(filePath, CONFIG_WITH_STATUS_IGNORED);
  840. return CONFIG_WITH_STATUS_IGNORED;
  841. }
  842. // filePath isn't automatically ignored, so try to construct config
  843. const matchingConfigIndices = [];
  844. let matchFound = false;
  845. const universalPattern = /^\*$|\/\*{1,2}$/u;
  846. this.forEach((config, index) => {
  847. if (!config.files) {
  848. if (!config.ignores) {
  849. debug(`Anonymous universal config found for ${filePath}`);
  850. matchingConfigIndices.push(index);
  851. return;
  852. }
  853. if (pathMatchesIgnores(filePath, this.basePath, config)) {
  854. debug(
  855. `Matching config found for ${filePath} (based on ignores: ${config.ignores})`,
  856. );
  857. matchingConfigIndices.push(index);
  858. return;
  859. }
  860. debug(
  861. `Skipped config found for ${filePath} (based on ignores: ${config.ignores})`,
  862. );
  863. return;
  864. }
  865. /*
  866. * If a config has a files pattern * or patterns ending in /** or /*,
  867. * and the filePath only matches those patterns, then the config is only
  868. * applied if there is another config where the filePath matches
  869. * a file with a specific extensions such as *.js.
  870. */
  871. const universalFiles = config.files.filter(pattern =>
  872. universalPattern.test(pattern),
  873. );
  874. // universal patterns were found so we need to check the config twice
  875. if (universalFiles.length) {
  876. debug("Universal files patterns found. Checking carefully.");
  877. const nonUniversalFiles = config.files.filter(
  878. pattern => !universalPattern.test(pattern),
  879. );
  880. // check that the config matches without the non-universal files first
  881. if (
  882. nonUniversalFiles.length &&
  883. pathMatches(filePath, this.basePath, {
  884. files: nonUniversalFiles,
  885. ignores: config.ignores,
  886. })
  887. ) {
  888. debug(`Matching config found for ${filePath}`);
  889. matchingConfigIndices.push(index);
  890. matchFound = true;
  891. return;
  892. }
  893. // if there wasn't a match then check if it matches with universal files
  894. if (
  895. universalFiles.length &&
  896. pathMatches(filePath, this.basePath, {
  897. files: universalFiles,
  898. ignores: config.ignores,
  899. })
  900. ) {
  901. debug(`Matching config found for ${filePath}`);
  902. matchingConfigIndices.push(index);
  903. return;
  904. }
  905. // if we make here, then there was no match
  906. return;
  907. }
  908. // the normal case
  909. if (pathMatches(filePath, this.basePath, config)) {
  910. debug(`Matching config found for ${filePath}`);
  911. matchingConfigIndices.push(index);
  912. matchFound = true;
  913. }
  914. });
  915. // if matching both files and ignores, there will be no config to create
  916. if (!matchFound) {
  917. debug(`No matching configs found for ${filePath}`);
  918. // cache and return result
  919. cache.set(filePath, CONFIG_WITH_STATUS_UNCONFIGURED);
  920. return CONFIG_WITH_STATUS_UNCONFIGURED;
  921. }
  922. // check to see if there is a config cached by indices
  923. const indicesKey = matchingConfigIndices.toString();
  924. let configWithStatus = cache.get(indicesKey);
  925. if (configWithStatus) {
  926. // also store for filename for faster lookup next time
  927. cache.set(filePath, configWithStatus);
  928. return configWithStatus;
  929. }
  930. // otherwise construct the config
  931. // eslint-disable-next-line array-callback-return, consistent-return -- rethrowConfigError always throws an error
  932. let finalConfig = matchingConfigIndices.reduce((result, index) => {
  933. try {
  934. return this[ConfigArraySymbol.schema].merge(
  935. result,
  936. this[index],
  937. );
  938. } catch (validationError) {
  939. rethrowConfigError(this[index], index, validationError);
  940. }
  941. }, {});
  942. finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig);
  943. configWithStatus = Object.freeze({
  944. config: finalConfig,
  945. status: "matched",
  946. });
  947. cache.set(filePath, configWithStatus);
  948. cache.set(indicesKey, configWithStatus);
  949. return configWithStatus;
  950. }
  951. /**
  952. * Returns the config object for a given file path.
  953. * @param {string} filePath The complete path of a file to get a config for.
  954. * @returns {Object|undefined} The config object for this file or `undefined`.
  955. */
  956. getConfig(filePath) {
  957. return this.getConfigWithStatus(filePath).config;
  958. }
  959. /**
  960. * Determines whether a file has a config or why it doesn't.
  961. * @param {string} filePath The complete path of the file to check.
  962. * @returns {"ignored"|"external"|"unconfigured"|"matched"} One of the following values:
  963. * * `"ignored"`: the file is ignored
  964. * * `"external"`: the file is outside the base path
  965. * * `"unconfigured"`: the file is not matched by any config
  966. * * `"matched"`: the file has a matching config
  967. */
  968. getConfigStatus(filePath) {
  969. return this.getConfigWithStatus(filePath).status;
  970. }
  971. /**
  972. * Determines if the given filepath is ignored based on the configs.
  973. * @param {string} filePath The complete path of a file to check.
  974. * @returns {boolean} True if the path is ignored, false if not.
  975. * @deprecated Use `isFileIgnored` instead.
  976. */
  977. isIgnored(filePath) {
  978. return this.isFileIgnored(filePath);
  979. }
  980. /**
  981. * Determines if the given filepath is ignored based on the configs.
  982. * @param {string} filePath The complete path of a file to check.
  983. * @returns {boolean} True if the path is ignored, false if not.
  984. */
  985. isFileIgnored(filePath) {
  986. return this.getConfigStatus(filePath) === "ignored";
  987. }
  988. /**
  989. * Determines if the given directory is ignored based on the configs.
  990. * This checks only default `ignores` that don't have `files` in the
  991. * same config. A pattern such as `/foo` be considered to ignore the directory
  992. * while a pattern such as `/foo/**` is not considered to ignore the
  993. * directory because it is matching files.
  994. * @param {string} directoryPath The complete path of a directory to check.
  995. * @returns {boolean} True if the directory is ignored, false if not. Will
  996. * return true for any directory that is not inside of `basePath`.
  997. * @throws {Error} When the `ConfigArray` is not normalized.
  998. */
  999. isDirectoryIgnored(directoryPath) {
  1000. assertNormalized(this);
  1001. const relativeDirectoryPath = path
  1002. .relative(this.basePath, directoryPath)
  1003. .replace(/\\/gu, "/");
  1004. // basePath directory can never be ignored
  1005. if (relativeDirectoryPath === "") {
  1006. return false;
  1007. }
  1008. if (relativeDirectoryPath.startsWith("..")) {
  1009. return true;
  1010. }
  1011. // first check the cache
  1012. const cache = dataCache.get(this).directoryMatches;
  1013. if (cache.has(relativeDirectoryPath)) {
  1014. return cache.get(relativeDirectoryPath);
  1015. }
  1016. const directoryParts = relativeDirectoryPath.split("/");
  1017. let relativeDirectoryToCheck = "";
  1018. let result;
  1019. /*
  1020. * In order to get the correct gitignore-style ignores, where an
  1021. * ignored parent directory cannot have any descendants unignored,
  1022. * we need to check every directory starting at the parent all
  1023. * the way down to the actual requested directory.
  1024. *
  1025. * We aggressively cache all of this info to make sure we don't
  1026. * have to recalculate everything for every call.
  1027. */
  1028. do {
  1029. relativeDirectoryToCheck += `${directoryParts.shift()}/`;
  1030. result = shouldIgnorePath(
  1031. this.ignores,
  1032. path.join(this.basePath, relativeDirectoryToCheck),
  1033. relativeDirectoryToCheck,
  1034. );
  1035. cache.set(relativeDirectoryToCheck, result);
  1036. } while (!result && directoryParts.length);
  1037. // also cache the result for the requested path
  1038. cache.set(relativeDirectoryPath, result);
  1039. return result;
  1040. }
  1041. }
  1042. Object.defineProperty(exports, "ObjectSchema", {
  1043. enumerable: true,
  1044. get: function () { return objectSchema.ObjectSchema; }
  1045. });
  1046. exports.ConfigArray = ConfigArray;
  1047. exports.ConfigArraySymbol = ConfigArraySymbol;