/** * @fileoverview `ConfigArray` class. * * `ConfigArray` class expresses the full of a configuration. It has the entry * config file, base config files that were extended, loaded parsers, and loaded * plugins. * * `ConfigArray` class provides three properties and two methods. * * - `pluginEnvironments` * - `pluginProcessors` * - `pluginRules` * The `Map` objects that contain the members of all plugins that this * config array contains. Those map objects don't have mutation methods. * Those keys are the member ID such as `pluginId/memberName`. * - `isRoot()` * If `true` then this configuration has `root:true` property. * - `extractConfig(filePath)` * Extract the final configuration for a given file. This means merging * every config array element which that `criteria` property matched. The * `filePath` argument must be an absolute path. * * `ConfigArrayFactory` provides the loading logic of config files. * * @author Toru Nagashima */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ import { ExtractedConfig } from "./extracted-config.js"; import { IgnorePattern } from "./ignore-pattern.js"; //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ // Define types for VSCode IntelliSense. /** @typedef {import("../../shared/types").Environment} Environment */ /** @typedef {import("../../shared/types").GlobalConf} GlobalConf */ /** @typedef {import("../../shared/types").RuleConf} RuleConf */ /** @typedef {import("../../shared/types").Rule} Rule */ /** @typedef {import("../../shared/types").Plugin} Plugin */ /** @typedef {import("../../shared/types").Processor} Processor */ /** @typedef {import("./config-dependency").DependentParser} DependentParser */ /** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */ /** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */ /** * @typedef {Object} ConfigArrayElement * @property {string} name The name of this config element. * @property {string} filePath The path to the source file of this config element. * @property {InstanceType|null} criteria The tester for the `files` and `excludedFiles` of this config element. * @property {Record|undefined} env The environment settings. * @property {Record|undefined} globals The global variable settings. * @property {IgnorePattern|undefined} ignorePattern The ignore patterns. * @property {boolean|undefined} noInlineConfig The flag that disables directive comments. * @property {DependentParser|undefined} parser The parser loader. * @property {Object|undefined} parserOptions The parser options. * @property {Record|undefined} plugins The plugin loaders. * @property {string|undefined} processor The processor name to refer plugin's processor. * @property {boolean|undefined} reportUnusedDisableDirectives The flag to report unused `eslint-disable` comments. * @property {boolean|undefined} root The flag to express root. * @property {Record|undefined} rules The rule settings * @property {Object|undefined} settings The shared settings. * @property {"config" | "ignore" | "implicit-processor"} type The element type. */ /** * @typedef {Object} ConfigArrayInternalSlots * @property {Map} cache The cache to extract configs. * @property {ReadonlyMap|null} envMap The map from environment ID to environment definition. * @property {ReadonlyMap|null} processorMap The map from processor ID to environment definition. * @property {ReadonlyMap|null} ruleMap The map from rule ID to rule definition. */ /** @type {WeakMap} */ const internalSlotsMap = new class extends WeakMap { get(key) { let value = super.get(key); if (!value) { value = { cache: new Map(), envMap: null, processorMap: null, ruleMap: null }; super.set(key, value); } return value; } }(); /** * Get the indices which are matched to a given file. * @param {ConfigArrayElement[]} elements The elements. * @param {string} filePath The path to a target file. * @returns {number[]} The indices. */ function getMatchedIndices(elements, filePath) { const indices = []; for (let i = elements.length - 1; i >= 0; --i) { const element = elements[i]; if (!element.criteria || (filePath && element.criteria.test(filePath))) { indices.push(i); } } return indices; } /** * Check if a value is a non-null object. * @param {any} x The value to check. * @returns {boolean} `true` if the value is a non-null object. */ function isNonNullObject(x) { return typeof x === "object" && x !== null; } /** * Merge two objects. * * Assign every property values of `y` to `x` if `x` doesn't have the property. * If `x`'s property value is an object, it does recursive. * @param {Object} target The destination to merge * @param {Object|undefined} source The source to merge. * @returns {void} */ function mergeWithoutOverwrite(target, source) { if (!isNonNullObject(source)) { return; } for (const key of Object.keys(source)) { if (key === "__proto__") { continue; } if (isNonNullObject(target[key])) { mergeWithoutOverwrite(target[key], source[key]); } else if (target[key] === void 0) { if (isNonNullObject(source[key])) { target[key] = Array.isArray(source[key]) ? [] : {}; mergeWithoutOverwrite(target[key], source[key]); } else if (source[key] !== void 0) { target[key] = source[key]; } } } } /** * The error for plugin conflicts. */ class PluginConflictError extends Error { /** * Initialize this error object. * @param {string} pluginId The plugin ID. * @param {{filePath:string, importerName:string}[]} plugins The resolved plugins. */ constructor(pluginId, plugins) { super(`Plugin "${pluginId}" was conflicted between ${plugins.map(p => `"${p.importerName}"`).join(" and ")}.`); this.messageTemplate = "plugin-conflict"; this.messageData = { pluginId, plugins }; } } /** * Merge plugins. * `target`'s definition is prior to `source`'s. * @param {Record} target The destination to merge * @param {Record|undefined} source The source to merge. * @returns {void} */ function mergePlugins(target, source) { if (!isNonNullObject(source)) { return; } for (const key of Object.keys(source)) { if (key === "__proto__") { continue; } const targetValue = target[key]; const sourceValue = source[key]; // Adopt the plugin which was found at first. if (targetValue === void 0) { if (sourceValue.error) { throw sourceValue.error; } target[key] = sourceValue; } else if (sourceValue.filePath !== targetValue.filePath) { throw new PluginConflictError(key, [ { filePath: targetValue.filePath, importerName: targetValue.importerName }, { filePath: sourceValue.filePath, importerName: sourceValue.importerName } ]); } } } /** * Merge rule configs. * `target`'s definition is prior to `source`'s. * @param {Record} target The destination to merge * @param {Record|undefined} source The source to merge. * @returns {void} */ function mergeRuleConfigs(target, source) { if (!isNonNullObject(source)) { return; } for (const key of Object.keys(source)) { if (key === "__proto__") { continue; } const targetDef = target[key]; const sourceDef = source[key]; // Adopt the rule config which was found at first. if (targetDef === void 0) { if (Array.isArray(sourceDef)) { target[key] = [...sourceDef]; } else { target[key] = [sourceDef]; } /* * If the first found rule config is severity only and the current rule * config has options, merge the severity and the options. */ } else if ( targetDef.length === 1 && Array.isArray(sourceDef) && sourceDef.length >= 2 ) { targetDef.push(...sourceDef.slice(1)); } } } /** * Create the extracted config. * @param {ConfigArray} instance The config elements. * @param {number[]} indices The indices to use. * @returns {ExtractedConfig} The extracted config. */ function createConfig(instance, indices) { const config = new ExtractedConfig(); const ignorePatterns = []; // Merge elements. for (const index of indices) { const element = instance[index]; // Adopt the parser which was found at first. if (!config.parser && element.parser) { if (element.parser.error) { throw element.parser.error; } config.parser = element.parser; } // Adopt the processor which was found at first. if (!config.processor && element.processor) { config.processor = element.processor; } // Adopt the noInlineConfig which was found at first. if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) { config.noInlineConfig = element.noInlineConfig; config.configNameOfNoInlineConfig = element.name; } // Adopt the reportUnusedDisableDirectives which was found at first. if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) { config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives; } // Collect ignorePatterns if (element.ignorePattern) { ignorePatterns.push(element.ignorePattern); } // Merge others. mergeWithoutOverwrite(config.env, element.env); mergeWithoutOverwrite(config.globals, element.globals); mergeWithoutOverwrite(config.parserOptions, element.parserOptions); mergeWithoutOverwrite(config.settings, element.settings); mergePlugins(config.plugins, element.plugins); mergeRuleConfigs(config.rules, element.rules); } // Create the predicate function for ignore patterns. if (ignorePatterns.length > 0) { config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse()); } return config; } /** * Collect definitions. * @template T, U * @param {string} pluginId The plugin ID for prefix. * @param {Record} defs The definitions to collect. * @param {Map} map The map to output. * @returns {void} */ function collect(pluginId, defs, map) { if (defs) { const prefix = pluginId && `${pluginId}/`; for (const [key, value] of Object.entries(defs)) { map.set(`${prefix}${key}`, value); } } } /** * Delete the mutation methods from a given map. * @param {Map} map The map object to delete. * @returns {void} */ function deleteMutationMethods(map) { Object.defineProperties(map, { clear: { configurable: true, value: void 0 }, delete: { configurable: true, value: void 0 }, set: { configurable: true, value: void 0 } }); } /** * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array. * @param {ConfigArrayElement[]} elements The config elements. * @param {ConfigArrayInternalSlots} slots The internal slots. * @returns {void} */ function initPluginMemberMaps(elements, slots) { const processed = new Set(); slots.envMap = new Map(); slots.processorMap = new Map(); slots.ruleMap = new Map(); for (const element of elements) { if (!element.plugins) { continue; } for (const [pluginId, value] of Object.entries(element.plugins)) { const plugin = value.definition; if (!plugin || processed.has(pluginId)) { continue; } processed.add(pluginId); collect(pluginId, plugin.environments, slots.envMap); collect(pluginId, plugin.processors, slots.processorMap); collect(pluginId, plugin.rules, slots.ruleMap); } } deleteMutationMethods(slots.envMap); deleteMutationMethods(slots.processorMap); deleteMutationMethods(slots.ruleMap); } /** * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array. * @param {ConfigArray} instance The config elements. * @returns {ConfigArrayInternalSlots} The extracted config. */ function ensurePluginMemberMaps(instance) { const slots = internalSlotsMap.get(instance); if (!slots.ruleMap) { initPluginMemberMaps(instance, slots); } return slots; } //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * The Config Array. * * `ConfigArray` instance contains all settings, parsers, and plugins. * You need to call `ConfigArray#extractConfig(filePath)` method in order to * extract, merge and get only the config data which is related to an arbitrary * file. * @extends {Array} */ class ConfigArray extends Array { /** * Get the plugin environments. * The returned map cannot be mutated. * @type {ReadonlyMap} The plugin environments. */ get pluginEnvironments() { return ensurePluginMemberMaps(this).envMap; } /** * Get the plugin processors. * The returned map cannot be mutated. * @type {ReadonlyMap} The plugin processors. */ get pluginProcessors() { return ensurePluginMemberMaps(this).processorMap; } /** * Get the plugin rules. * The returned map cannot be mutated. * @returns {ReadonlyMap} The plugin rules. */ get pluginRules() { return ensurePluginMemberMaps(this).ruleMap; } /** * Check if this config has `root` flag. * @returns {boolean} `true` if this config array is root. */ isRoot() { for (let i = this.length - 1; i >= 0; --i) { const root = this[i].root; if (typeof root === "boolean") { return root; } } return false; } /** * Extract the config data which is related to a given file. * @param {string} filePath The absolute path to the target file. * @returns {ExtractedConfig} The extracted config data. */ extractConfig(filePath) { const { cache } = internalSlotsMap.get(this); const indices = getMatchedIndices(this, filePath); const cacheKey = indices.join(","); if (!cache.has(cacheKey)) { cache.set(cacheKey, createConfig(this, indices)); } return cache.get(cacheKey); } /** * Check if a given path is an additional lint target. * @param {string} filePath The absolute path to the target file. * @returns {boolean} `true` if the file is an additional lint target. */ isAdditionalTargetPath(filePath) { for (const { criteria, type } of this) { if ( type === "config" && criteria && !criteria.endsWithWildcard && criteria.test(filePath) ) { return true; } } return false; } } /** * Get the used extracted configs. * CLIEngine will use this method to collect used deprecated rules. * @param {ConfigArray} instance The config array object to get. * @returns {ExtractedConfig[]} The used extracted configs. * @private */ function getUsedExtractedConfigs(instance) { const { cache } = internalSlotsMap.get(instance); return Array.from(cache.values()); } export { ConfigArray, getUsedExtractedConfigs };