index.js 34 KB

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