index.js 42 KB

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