2 * @fileoverview `ConfigArray` class.
4 * `ConfigArray` class expresses the full of a configuration. It has the entry
5 * config file, base config files that were extended, loaded parsers, and loaded
8 * `ConfigArray` class provies three properties and two methods.
10 * - `pluginEnvironments`
11 * - `pluginProcessors`
13 * The `Map` objects that contain the members of all plugins that this
14 * config array contains. Those map objects don't have mutation methods.
15 * Those keys are the member ID such as `pluginId/memberName`.
17 * If `true` then this configuration has `root:true` property.
18 * - `extractConfig(filePath)`
19 * Extract the final configuration for a given file. This means merging
20 * every config array element which that `criteria` property matched. The
21 * `filePath` argument must be an absolute path.
23 * `ConfigArrayFactory` provides the loading logic of config files.
25 * @author Toru Nagashima <https://github.com/mysticatea>
29 //------------------------------------------------------------------------------
31 //------------------------------------------------------------------------------
33 const { ExtractedConfig } = require("./extracted-config");
34 const { IgnorePattern } = require("./ignore-pattern");
36 //------------------------------------------------------------------------------
38 //------------------------------------------------------------------------------
40 // Define types for VSCode IntelliSense.
41 /** @typedef {import("../../shared/types").Environment} Environment */
42 /** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
43 /** @typedef {import("../../shared/types").RuleConf} RuleConf */
44 /** @typedef {import("../../shared/types").Rule} Rule */
45 /** @typedef {import("../../shared/types").Plugin} Plugin */
46 /** @typedef {import("../../shared/types").Processor} Processor */
47 /** @typedef {import("./config-dependency").DependentParser} DependentParser */
48 /** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
49 /** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */
52 * @typedef {Object} ConfigArrayElement
53 * @property {string} name The name of this config element.
54 * @property {string} filePath The path to the source file of this config element.
55 * @property {InstanceType<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element.
56 * @property {Record<string, boolean>|undefined} env The environment settings.
57 * @property {Record<string, GlobalConf>|undefined} globals The global variable settings.
58 * @property {IgnorePattern|undefined} ignorePattern The ignore patterns.
59 * @property {boolean|undefined} noInlineConfig The flag that disables directive comments.
60 * @property {DependentParser|undefined} parser The parser loader.
61 * @property {Object|undefined} parserOptions The parser options.
62 * @property {Record<string, DependentPlugin>|undefined} plugins The plugin loaders.
63 * @property {string|undefined} processor The processor name to refer plugin's processor.
64 * @property {boolean|undefined} reportUnusedDisableDirectives The flag to report unused `eslint-disable` comments.
65 * @property {boolean|undefined} root The flag to express root.
66 * @property {Record<string, RuleConf>|undefined} rules The rule settings
67 * @property {Object|undefined} settings The shared settings.
71 * @typedef {Object} ConfigArrayInternalSlots
72 * @property {Map<string, ExtractedConfig>} cache The cache to extract configs.
73 * @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition.
74 * @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition.
75 * @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition.
78 /** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */
79 const internalSlotsMap = new class extends WeakMap {
81 let value = super.get(key);
90 super.set(key, value);
98 * Get the indices which are matched to a given file.
99 * @param {ConfigArrayElement[]} elements The elements.
100 * @param {string} filePath The path to a target file.
101 * @returns {number[]} The indices.
103 function getMatchedIndices(elements, filePath) {
106 for (let i = elements.length - 1; i >= 0; --i) {
107 const element = elements[i];
109 if (!element.criteria || element.criteria.test(filePath)) {
118 * Check if a value is a non-null object.
119 * @param {any} x The value to check.
120 * @returns {boolean} `true` if the value is a non-null object.
122 function isNonNullObject(x) {
123 return typeof x === "object" && x !== null;
129 * Assign every property values of `y` to `x` if `x` doesn't have the property.
130 * If `x`'s property value is an object, it does recursive.
131 * @param {Object} target The destination to merge
132 * @param {Object|undefined} source The source to merge.
135 function mergeWithoutOverwrite(target, source) {
136 if (!isNonNullObject(source)) {
140 for (const key of Object.keys(source)) {
141 if (key === "__proto__") {
145 if (isNonNullObject(target[key])) {
146 mergeWithoutOverwrite(target[key], source[key]);
147 } else if (target[key] === void 0) {
148 if (isNonNullObject(source[key])) {
149 target[key] = Array.isArray(source[key]) ? [] : {};
150 mergeWithoutOverwrite(target[key], source[key]);
151 } else if (source[key] !== void 0) {
152 target[key] = source[key];
160 * `target`'s definition is prior to `source`'s.
161 * @param {Record<string, DependentPlugin>} target The destination to merge
162 * @param {Record<string, DependentPlugin>|undefined} source The source to merge.
165 function mergePlugins(target, source) {
166 if (!isNonNullObject(source)) {
170 for (const key of Object.keys(source)) {
171 if (key === "__proto__") {
174 const targetValue = target[key];
175 const sourceValue = source[key];
177 // Adopt the plugin which was found at first.
178 if (targetValue === void 0) {
179 if (sourceValue.error) {
180 throw sourceValue.error;
182 target[key] = sourceValue;
188 * Merge rule configs.
189 * `target`'s definition is prior to `source`'s.
190 * @param {Record<string, Array>} target The destination to merge
191 * @param {Record<string, RuleConf>|undefined} source The source to merge.
194 function mergeRuleConfigs(target, source) {
195 if (!isNonNullObject(source)) {
199 for (const key of Object.keys(source)) {
200 if (key === "__proto__") {
203 const targetDef = target[key];
204 const sourceDef = source[key];
206 // Adopt the rule config which was found at first.
207 if (targetDef === void 0) {
208 if (Array.isArray(sourceDef)) {
209 target[key] = [...sourceDef];
211 target[key] = [sourceDef];
215 * If the first found rule config is severity only and the current rule
216 * config has options, merge the severity and the options.
219 targetDef.length === 1 &&
220 Array.isArray(sourceDef) &&
221 sourceDef.length >= 2
223 targetDef.push(...sourceDef.slice(1));
229 * Create the extracted config.
230 * @param {ConfigArray} instance The config elements.
231 * @param {number[]} indices The indices to use.
232 * @returns {ExtractedConfig} The extracted config.
234 function createConfig(instance, indices) {
235 const config = new ExtractedConfig();
236 const ignorePatterns = [];
239 for (const index of indices) {
240 const element = instance[index];
242 // Adopt the parser which was found at first.
243 if (!config.parser && element.parser) {
244 if (element.parser.error) {
245 throw element.parser.error;
247 config.parser = element.parser;
250 // Adopt the processor which was found at first.
251 if (!config.processor && element.processor) {
252 config.processor = element.processor;
255 // Adopt the noInlineConfig which was found at first.
256 if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) {
257 config.noInlineConfig = element.noInlineConfig;
258 config.configNameOfNoInlineConfig = element.name;
261 // Adopt the reportUnusedDisableDirectives which was found at first.
262 if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) {
263 config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives;
266 // Collect ignorePatterns
267 if (element.ignorePattern) {
268 ignorePatterns.push(element.ignorePattern);
272 mergeWithoutOverwrite(config.env, element.env);
273 mergeWithoutOverwrite(config.globals, element.globals);
274 mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
275 mergeWithoutOverwrite(config.settings, element.settings);
276 mergePlugins(config.plugins, element.plugins);
277 mergeRuleConfigs(config.rules, element.rules);
280 // Create the predicate function for ignore patterns.
281 if (ignorePatterns.length > 0) {
282 config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse());
289 * Collect definitions.
291 * @param {string} pluginId The plugin ID for prefix.
292 * @param {Record<string,T>} defs The definitions to collect.
293 * @param {Map<string, U>} map The map to output.
294 * @param {function(T): U} [normalize] The normalize function for each value.
297 function collect(pluginId, defs, map, normalize) {
299 const prefix = pluginId && `${pluginId}/`;
301 for (const [key, value] of Object.entries(defs)) {
304 normalize ? normalize(value) : value
311 * Normalize a rule definition.
312 * @param {Function|Rule} rule The rule definition to normalize.
313 * @returns {Rule} The normalized rule definition.
315 function normalizePluginRule(rule) {
316 return typeof rule === "function" ? { create: rule } : rule;
320 * Delete the mutation methods from a given map.
321 * @param {Map<any, any>} map The map object to delete.
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 }
333 * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
334 * @param {ConfigArrayElement[]} elements The config elements.
335 * @param {ConfigArrayInternalSlots} slots The internal slots.
338 function initPluginMemberMaps(elements, slots) {
339 const processed = new Set();
341 slots.envMap = new Map();
342 slots.processorMap = new Map();
343 slots.ruleMap = new Map();
345 for (const element of elements) {
346 if (!element.plugins) {
350 for (const [pluginId, value] of Object.entries(element.plugins)) {
351 const plugin = value.definition;
353 if (!plugin || processed.has(pluginId)) {
356 processed.add(pluginId);
358 collect(pluginId, plugin.environments, slots.envMap);
359 collect(pluginId, plugin.processors, slots.processorMap);
360 collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule);
364 deleteMutationMethods(slots.envMap);
365 deleteMutationMethods(slots.processorMap);
366 deleteMutationMethods(slots.ruleMap);
370 * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
371 * @param {ConfigArray} instance The config elements.
372 * @returns {ConfigArrayInternalSlots} The extracted config.
374 function ensurePluginMemberMaps(instance) {
375 const slots = internalSlotsMap.get(instance);
377 if (!slots.ruleMap) {
378 initPluginMemberMaps(instance, slots);
384 //------------------------------------------------------------------------------
386 //------------------------------------------------------------------------------
391 * `ConfigArray` instance contains all settings, parsers, and plugins.
392 * You need to call `ConfigArray#extractConfig(filePath)` method in order to
393 * extract, merge and get only the config data which is related to an arbitrary
395 * @extends {Array<ConfigArrayElement>}
397 class ConfigArray extends Array {
400 * Get the plugin environments.
401 * The returned map cannot be mutated.
402 * @type {ReadonlyMap<string, Environment>} The plugin environments.
404 get pluginEnvironments() {
405 return ensurePluginMemberMaps(this).envMap;
409 * Get the plugin processors.
410 * The returned map cannot be mutated.
411 * @type {ReadonlyMap<string, Processor>} The plugin processors.
413 get pluginProcessors() {
414 return ensurePluginMemberMaps(this).processorMap;
418 * Get the plugin rules.
419 * The returned map cannot be mutated.
420 * @returns {ReadonlyMap<string, Rule>} The plugin rules.
423 return ensurePluginMemberMaps(this).ruleMap;
427 * Check if this config has `root` flag.
428 * @returns {boolean} `true` if this config array is root.
431 for (let i = this.length - 1; i >= 0; --i) {
432 const root = this[i].root;
434 if (typeof root === "boolean") {
442 * Extract the config data which is related to a given file.
443 * @param {string} filePath The absolute path to the target file.
444 * @returns {ExtractedConfig} The extracted config data.
446 extractConfig(filePath) {
447 const { cache } = internalSlotsMap.get(this);
448 const indices = getMatchedIndices(this, filePath);
449 const cacheKey = indices.join(",");
451 if (!cache.has(cacheKey)) {
452 cache.set(cacheKey, createConfig(this, indices));
455 return cache.get(cacheKey);
459 const exportObject = {
463 * Get the used extracted configs.
464 * CLIEngine will use this method to collect used deprecated rules.
465 * @param {ConfigArray} instance The config array object to get.
466 * @returns {ExtractedConfig[]} The used extracted configs.
469 getUsedExtractedConfigs(instance) {
470 const { cache } = internalSlotsMap.get(instance);
472 return Array.from(cache.values());
476 module.exports = exportObject;