config-array.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. /*
  2. * STOP!!! DO NOT MODIFY.
  3. *
  4. * This file is part of the ongoing work to move the eslintrc-style config
  5. * system into the @eslint/eslintrc package. This file needs to remain
  6. * unchanged in order for this work to proceed.
  7. *
  8. * If you think you need to change this file, please contact @nzakas first.
  9. *
  10. * Thanks in advance for your cooperation.
  11. */
  12. /**
  13. * @fileoverview `ConfigArray` class.
  14. *
  15. * `ConfigArray` class expresses the full of a configuration. It has the entry
  16. * config file, base config files that were extended, loaded parsers, and loaded
  17. * plugins.
  18. *
  19. * `ConfigArray` class provides three properties and two methods.
  20. *
  21. * - `pluginEnvironments`
  22. * - `pluginProcessors`
  23. * - `pluginRules`
  24. * The `Map` objects that contain the members of all plugins that this
  25. * config array contains. Those map objects don't have mutation methods.
  26. * Those keys are the member ID such as `pluginId/memberName`.
  27. * - `isRoot()`
  28. * If `true` then this configuration has `root:true` property.
  29. * - `extractConfig(filePath)`
  30. * Extract the final configuration for a given file. This means merging
  31. * every config array element which that `criteria` property matched. The
  32. * `filePath` argument must be an absolute path.
  33. *
  34. * `ConfigArrayFactory` provides the loading logic of config files.
  35. *
  36. * @author Toru Nagashima <https://github.com/mysticatea>
  37. */
  38. "use strict";
  39. //------------------------------------------------------------------------------
  40. // Requirements
  41. //------------------------------------------------------------------------------
  42. const { ExtractedConfig } = require("./extracted-config");
  43. const { IgnorePattern } = require("./ignore-pattern");
  44. //------------------------------------------------------------------------------
  45. // Helpers
  46. //------------------------------------------------------------------------------
  47. // Define types for VSCode IntelliSense.
  48. /** @typedef {import("../../shared/types").Environment} Environment */
  49. /** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
  50. /** @typedef {import("../../shared/types").RuleConf} RuleConf */
  51. /** @typedef {import("../../shared/types").Rule} Rule */
  52. /** @typedef {import("../../shared/types").Plugin} Plugin */
  53. /** @typedef {import("../../shared/types").Processor} Processor */
  54. /** @typedef {import("./config-dependency").DependentParser} DependentParser */
  55. /** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
  56. /** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */
  57. /**
  58. * @typedef {Object} ConfigArrayElement
  59. * @property {string} name The name of this config element.
  60. * @property {string} filePath The path to the source file of this config element.
  61. * @property {InstanceType<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element.
  62. * @property {Record<string, boolean>|undefined} env The environment settings.
  63. * @property {Record<string, GlobalConf>|undefined} globals The global variable settings.
  64. * @property {IgnorePattern|undefined} ignorePattern The ignore patterns.
  65. * @property {boolean|undefined} noInlineConfig The flag that disables directive comments.
  66. * @property {DependentParser|undefined} parser The parser loader.
  67. * @property {Object|undefined} parserOptions The parser options.
  68. * @property {Record<string, DependentPlugin>|undefined} plugins The plugin loaders.
  69. * @property {string|undefined} processor The processor name to refer plugin's processor.
  70. * @property {boolean|undefined} reportUnusedDisableDirectives The flag to report unused `eslint-disable` comments.
  71. * @property {boolean|undefined} root The flag to express root.
  72. * @property {Record<string, RuleConf>|undefined} rules The rule settings
  73. * @property {Object|undefined} settings The shared settings.
  74. * @property {"config" | "ignore" | "implicit-processor"} type The element type.
  75. */
  76. /**
  77. * @typedef {Object} ConfigArrayInternalSlots
  78. * @property {Map<string, ExtractedConfig>} cache The cache to extract configs.
  79. * @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition.
  80. * @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition.
  81. * @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition.
  82. */
  83. /** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */
  84. const internalSlotsMap = new class extends WeakMap {
  85. get(key) {
  86. let value = super.get(key);
  87. if (!value) {
  88. value = {
  89. cache: new Map(),
  90. envMap: null,
  91. processorMap: null,
  92. ruleMap: null
  93. };
  94. super.set(key, value);
  95. }
  96. return value;
  97. }
  98. }();
  99. /**
  100. * Get the indices which are matched to a given file.
  101. * @param {ConfigArrayElement[]} elements The elements.
  102. * @param {string} filePath The path to a target file.
  103. * @returns {number[]} The indices.
  104. */
  105. function getMatchedIndices(elements, filePath) {
  106. const indices = [];
  107. for (let i = elements.length - 1; i >= 0; --i) {
  108. const element = elements[i];
  109. if (!element.criteria || (filePath && element.criteria.test(filePath))) {
  110. indices.push(i);
  111. }
  112. }
  113. return indices;
  114. }
  115. /**
  116. * Check if a value is a non-null object.
  117. * @param {any} x The value to check.
  118. * @returns {boolean} `true` if the value is a non-null object.
  119. */
  120. function isNonNullObject(x) {
  121. return typeof x === "object" && x !== null;
  122. }
  123. /**
  124. * Merge two objects.
  125. *
  126. * Assign every property values of `y` to `x` if `x` doesn't have the property.
  127. * If `x`'s property value is an object, it does recursive.
  128. * @param {Object} target The destination to merge
  129. * @param {Object|undefined} source The source to merge.
  130. * @returns {void}
  131. */
  132. function mergeWithoutOverwrite(target, source) {
  133. if (!isNonNullObject(source)) {
  134. return;
  135. }
  136. for (const key of Object.keys(source)) {
  137. if (key === "__proto__") {
  138. continue;
  139. }
  140. if (isNonNullObject(target[key])) {
  141. mergeWithoutOverwrite(target[key], source[key]);
  142. } else if (target[key] === void 0) {
  143. if (isNonNullObject(source[key])) {
  144. target[key] = Array.isArray(source[key]) ? [] : {};
  145. mergeWithoutOverwrite(target[key], source[key]);
  146. } else if (source[key] !== void 0) {
  147. target[key] = source[key];
  148. }
  149. }
  150. }
  151. }
  152. /**
  153. * The error for plugin conflicts.
  154. */
  155. class PluginConflictError extends Error {
  156. /**
  157. * Initialize this error object.
  158. * @param {string} pluginId The plugin ID.
  159. * @param {{filePath:string, importerName:string}[]} plugins The resolved plugins.
  160. */
  161. constructor(pluginId, plugins) {
  162. super(`Plugin "${pluginId}" was conflicted between ${plugins.map(p => `"${p.importerName}"`).join(" and ")}.`);
  163. this.messageTemplate = "plugin-conflict";
  164. this.messageData = { pluginId, plugins };
  165. }
  166. }
  167. /**
  168. * Merge plugins.
  169. * `target`'s definition is prior to `source`'s.
  170. * @param {Record<string, DependentPlugin>} target The destination to merge
  171. * @param {Record<string, DependentPlugin>|undefined} source The source to merge.
  172. * @returns {void}
  173. */
  174. function mergePlugins(target, source) {
  175. if (!isNonNullObject(source)) {
  176. return;
  177. }
  178. for (const key of Object.keys(source)) {
  179. if (key === "__proto__") {
  180. continue;
  181. }
  182. const targetValue = target[key];
  183. const sourceValue = source[key];
  184. // Adopt the plugin which was found at first.
  185. if (targetValue === void 0) {
  186. if (sourceValue.error) {
  187. throw sourceValue.error;
  188. }
  189. target[key] = sourceValue;
  190. } else if (sourceValue.filePath !== targetValue.filePath) {
  191. throw new PluginConflictError(key, [
  192. {
  193. filePath: targetValue.filePath,
  194. importerName: targetValue.importerName
  195. },
  196. {
  197. filePath: sourceValue.filePath,
  198. importerName: sourceValue.importerName
  199. }
  200. ]);
  201. }
  202. }
  203. }
  204. /**
  205. * Merge rule configs.
  206. * `target`'s definition is prior to `source`'s.
  207. * @param {Record<string, Array>} target The destination to merge
  208. * @param {Record<string, RuleConf>|undefined} source The source to merge.
  209. * @returns {void}
  210. */
  211. function mergeRuleConfigs(target, source) {
  212. if (!isNonNullObject(source)) {
  213. return;
  214. }
  215. for (const key of Object.keys(source)) {
  216. if (key === "__proto__") {
  217. continue;
  218. }
  219. const targetDef = target[key];
  220. const sourceDef = source[key];
  221. // Adopt the rule config which was found at first.
  222. if (targetDef === void 0) {
  223. if (Array.isArray(sourceDef)) {
  224. target[key] = [...sourceDef];
  225. } else {
  226. target[key] = [sourceDef];
  227. }
  228. /*
  229. * If the first found rule config is severity only and the current rule
  230. * config has options, merge the severity and the options.
  231. */
  232. } else if (
  233. targetDef.length === 1 &&
  234. Array.isArray(sourceDef) &&
  235. sourceDef.length >= 2
  236. ) {
  237. targetDef.push(...sourceDef.slice(1));
  238. }
  239. }
  240. }
  241. /**
  242. * Create the extracted config.
  243. * @param {ConfigArray} instance The config elements.
  244. * @param {number[]} indices The indices to use.
  245. * @returns {ExtractedConfig} The extracted config.
  246. */
  247. function createConfig(instance, indices) {
  248. const config = new ExtractedConfig();
  249. const ignorePatterns = [];
  250. // Merge elements.
  251. for (const index of indices) {
  252. const element = instance[index];
  253. // Adopt the parser which was found at first.
  254. if (!config.parser && element.parser) {
  255. if (element.parser.error) {
  256. throw element.parser.error;
  257. }
  258. config.parser = element.parser;
  259. }
  260. // Adopt the processor which was found at first.
  261. if (!config.processor && element.processor) {
  262. config.processor = element.processor;
  263. }
  264. // Adopt the noInlineConfig which was found at first.
  265. if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) {
  266. config.noInlineConfig = element.noInlineConfig;
  267. config.configNameOfNoInlineConfig = element.name;
  268. }
  269. // Adopt the reportUnusedDisableDirectives which was found at first.
  270. if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) {
  271. config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives;
  272. }
  273. // Collect ignorePatterns
  274. if (element.ignorePattern) {
  275. ignorePatterns.push(element.ignorePattern);
  276. }
  277. // Merge others.
  278. mergeWithoutOverwrite(config.env, element.env);
  279. mergeWithoutOverwrite(config.globals, element.globals);
  280. mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
  281. mergeWithoutOverwrite(config.settings, element.settings);
  282. mergePlugins(config.plugins, element.plugins);
  283. mergeRuleConfigs(config.rules, element.rules);
  284. }
  285. // Create the predicate function for ignore patterns.
  286. if (ignorePatterns.length > 0) {
  287. config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse());
  288. }
  289. return config;
  290. }
  291. /**
  292. * Collect definitions.
  293. * @template T, U
  294. * @param {string} pluginId The plugin ID for prefix.
  295. * @param {Record<string,T>} defs The definitions to collect.
  296. * @param {Map<string, U>} map The map to output.
  297. * @param {function(T): U} [normalize] The normalize function for each value.
  298. * @returns {void}
  299. */
  300. function collect(pluginId, defs, map, normalize) {
  301. if (defs) {
  302. const prefix = pluginId && `${pluginId}/`;
  303. for (const [key, value] of Object.entries(defs)) {
  304. map.set(
  305. `${prefix}${key}`,
  306. normalize ? normalize(value) : value
  307. );
  308. }
  309. }
  310. }
  311. /**
  312. * Normalize a rule definition.
  313. * @param {Function|Rule} rule The rule definition to normalize.
  314. * @returns {Rule} The normalized rule definition.
  315. */
  316. function normalizePluginRule(rule) {
  317. return typeof rule === "function" ? { create: rule } : rule;
  318. }
  319. /**
  320. * Delete the mutation methods from a given map.
  321. * @param {Map<any, any>} map The map object to delete.
  322. * @returns {void}
  323. */
  324. function deleteMutationMethods(map) {
  325. Object.defineProperties(map, {
  326. clear: { configurable: true, value: void 0 },
  327. delete: { configurable: true, value: void 0 },
  328. set: { configurable: true, value: void 0 }
  329. });
  330. }
  331. /**
  332. * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
  333. * @param {ConfigArrayElement[]} elements The config elements.
  334. * @param {ConfigArrayInternalSlots} slots The internal slots.
  335. * @returns {void}
  336. */
  337. function initPluginMemberMaps(elements, slots) {
  338. const processed = new Set();
  339. slots.envMap = new Map();
  340. slots.processorMap = new Map();
  341. slots.ruleMap = new Map();
  342. for (const element of elements) {
  343. if (!element.plugins) {
  344. continue;
  345. }
  346. for (const [pluginId, value] of Object.entries(element.plugins)) {
  347. const plugin = value.definition;
  348. if (!plugin || processed.has(pluginId)) {
  349. continue;
  350. }
  351. processed.add(pluginId);
  352. collect(pluginId, plugin.environments, slots.envMap);
  353. collect(pluginId, plugin.processors, slots.processorMap);
  354. collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule);
  355. }
  356. }
  357. deleteMutationMethods(slots.envMap);
  358. deleteMutationMethods(slots.processorMap);
  359. deleteMutationMethods(slots.ruleMap);
  360. }
  361. /**
  362. * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
  363. * @param {ConfigArray} instance The config elements.
  364. * @returns {ConfigArrayInternalSlots} The extracted config.
  365. */
  366. function ensurePluginMemberMaps(instance) {
  367. const slots = internalSlotsMap.get(instance);
  368. if (!slots.ruleMap) {
  369. initPluginMemberMaps(instance, slots);
  370. }
  371. return slots;
  372. }
  373. //------------------------------------------------------------------------------
  374. // Public Interface
  375. //------------------------------------------------------------------------------
  376. /**
  377. * The Config Array.
  378. *
  379. * `ConfigArray` instance contains all settings, parsers, and plugins.
  380. * You need to call `ConfigArray#extractConfig(filePath)` method in order to
  381. * extract, merge and get only the config data which is related to an arbitrary
  382. * file.
  383. * @extends {Array<ConfigArrayElement>}
  384. */
  385. class ConfigArray extends Array {
  386. /**
  387. * Get the plugin environments.
  388. * The returned map cannot be mutated.
  389. * @type {ReadonlyMap<string, Environment>} The plugin environments.
  390. */
  391. get pluginEnvironments() {
  392. return ensurePluginMemberMaps(this).envMap;
  393. }
  394. /**
  395. * Get the plugin processors.
  396. * The returned map cannot be mutated.
  397. * @type {ReadonlyMap<string, Processor>} The plugin processors.
  398. */
  399. get pluginProcessors() {
  400. return ensurePluginMemberMaps(this).processorMap;
  401. }
  402. /**
  403. * Get the plugin rules.
  404. * The returned map cannot be mutated.
  405. * @returns {ReadonlyMap<string, Rule>} The plugin rules.
  406. */
  407. get pluginRules() {
  408. return ensurePluginMemberMaps(this).ruleMap;
  409. }
  410. /**
  411. * Check if this config has `root` flag.
  412. * @returns {boolean} `true` if this config array is root.
  413. */
  414. isRoot() {
  415. for (let i = this.length - 1; i >= 0; --i) {
  416. const root = this[i].root;
  417. if (typeof root === "boolean") {
  418. return root;
  419. }
  420. }
  421. return false;
  422. }
  423. /**
  424. * Extract the config data which is related to a given file.
  425. * @param {string} filePath The absolute path to the target file.
  426. * @returns {ExtractedConfig} The extracted config data.
  427. */
  428. extractConfig(filePath) {
  429. const { cache } = internalSlotsMap.get(this);
  430. const indices = getMatchedIndices(this, filePath);
  431. const cacheKey = indices.join(",");
  432. if (!cache.has(cacheKey)) {
  433. cache.set(cacheKey, createConfig(this, indices));
  434. }
  435. return cache.get(cacheKey);
  436. }
  437. /**
  438. * Check if a given path is an additional lint target.
  439. * @param {string} filePath The absolute path to the target file.
  440. * @returns {boolean} `true` if the file is an additional lint target.
  441. */
  442. isAdditionalTargetPath(filePath) {
  443. for (const { criteria, type } of this) {
  444. if (
  445. type === "config" &&
  446. criteria &&
  447. !criteria.endsWithWildcard &&
  448. criteria.test(filePath)
  449. ) {
  450. return true;
  451. }
  452. }
  453. return false;
  454. }
  455. }
  456. const exportObject = {
  457. ConfigArray,
  458. /**
  459. * Get the used extracted configs.
  460. * CLIEngine will use this method to collect used deprecated rules.
  461. * @param {ConfigArray} instance The config array object to get.
  462. * @returns {ExtractedConfig[]} The used extracted configs.
  463. * @private
  464. */
  465. getUsedExtractedConfigs(instance) {
  466. const { cache } = internalSlotsMap.get(instance);
  467. return Array.from(cache.values());
  468. }
  469. };
  470. module.exports = exportObject;