You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
523 lines
17 KiB
523 lines
17 KiB
/** |
|
* @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 |
|
//------------------------------------------------------------------------------ |
|
|
|
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<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; |
|
} |
|
} |
|
|
|
/** |
|
* 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 |
|
};
|
|
|