Spaces:
Runtime error
Runtime error
/** | |
* @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 <https://github.com/mysticatea> | |
*/ | |
; | |
//------------------------------------------------------------------------------ | |
// Requirements | |
//------------------------------------------------------------------------------ | |
const { ExtractedConfig } = require("./extracted-config"); | |
const { IgnorePattern } = require("./ignore-pattern"); | |
//------------------------------------------------------------------------------ | |
// 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<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element. | |
* @property {Record<string, boolean>|undefined} env The environment settings. | |
* @property {Record<string, GlobalConf>|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<string, DependentPlugin>|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<string, RuleConf>|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<string, ExtractedConfig>} cache The cache to extract configs. | |
* @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition. | |
* @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition. | |
* @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition. | |
*/ | |
/** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */ | |
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<string, DependentPlugin>} target The destination to merge | |
* @param {Record<string, DependentPlugin>|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<string, Array>} target The destination to merge | |
* @param {Record<string, RuleConf>|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<string,T>} defs The definitions to collect. | |
* @param {Map<string, U>} map The map to output. | |
* @param {function(T): U} [normalize] The normalize function for each value. | |
* @returns {void} | |
*/ | |
function collect(pluginId, defs, map, normalize) { | |
if (defs) { | |
const prefix = pluginId && `${pluginId}/`; | |
for (const [key, value] of Object.entries(defs)) { | |
map.set( | |
`${prefix}${key}`, | |
normalize ? normalize(value) : value | |
); | |
} | |
} | |
} | |
/** | |
* Normalize a rule definition. | |
* @param {Function|Rule} rule The rule definition to normalize. | |
* @returns {Rule} The normalized rule definition. | |
*/ | |
function normalizePluginRule(rule) { | |
return typeof rule === "function" ? { create: rule } : rule; | |
} | |
/** | |
* Delete the mutation methods from a given map. | |
* @param {Map<any, any>} 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, normalizePluginRule); | |
} | |
} | |
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<ConfigArrayElement>} | |
*/ | |
class ConfigArray extends Array { | |
/** | |
* Get the plugin environments. | |
* The returned map cannot be mutated. | |
* @type {ReadonlyMap<string, Environment>} The plugin environments. | |
*/ | |
get pluginEnvironments() { | |
return ensurePluginMemberMaps(this).envMap; | |
} | |
/** | |
* Get the plugin processors. | |
* The returned map cannot be mutated. | |
* @type {ReadonlyMap<string, Processor>} The plugin processors. | |
*/ | |
get pluginProcessors() { | |
return ensurePluginMemberMaps(this).processorMap; | |
} | |
/** | |
* Get the plugin rules. | |
* The returned map cannot be mutated. | |
* @returns {ReadonlyMap<string, Rule>} 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; | |
} | |
} | |
const exportObject = { | |
ConfigArray, | |
/** | |
* 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 | |
*/ | |
getUsedExtractedConfigs(instance) { | |
const { cache } = internalSlotsMap.get(instance); | |
return Array.from(cache.values()); | |
} | |
}; | |
module.exports = exportObject; | |