298 lines
8.9 KiB
JavaScript
298 lines
8.9 KiB
JavaScript
|
/**
|
||
|
* @fileoverview The `Config` class
|
||
|
* @author Nicholas C. Zakas
|
||
|
*/
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Requirements
|
||
|
//-----------------------------------------------------------------------------
|
||
|
|
||
|
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
|
||
|
const { getRuleFromConfig } = require("./flat-config-helpers");
|
||
|
const { flatConfigSchema, hasMethod } = require("./flat-config-schema");
|
||
|
const { RuleValidator } = require("./rule-validator");
|
||
|
const { ObjectSchema } = require("@eslint/config-array");
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Helpers
|
||
|
//-----------------------------------------------------------------------------
|
||
|
|
||
|
const ruleValidator = new RuleValidator();
|
||
|
|
||
|
const severities = new Map([
|
||
|
[0, 0],
|
||
|
[1, 1],
|
||
|
[2, 2],
|
||
|
["off", 0],
|
||
|
["warn", 1],
|
||
|
["error", 2]
|
||
|
]);
|
||
|
|
||
|
/**
|
||
|
* Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
|
||
|
* @param {string} identifier The identifier to parse.
|
||
|
* @returns {{objectName: string, pluginName: string}} The parts of the plugin
|
||
|
* name.
|
||
|
*/
|
||
|
function splitPluginIdentifier(identifier) {
|
||
|
const parts = identifier.split("/");
|
||
|
|
||
|
return {
|
||
|
objectName: parts.pop(),
|
||
|
pluginName: parts.join("/")
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the name of an object in the config by reading its `meta` key.
|
||
|
* @param {Object} object The object to check.
|
||
|
* @returns {string?} The name of the object if found or `null` if there
|
||
|
* is no name.
|
||
|
*/
|
||
|
function getObjectId(object) {
|
||
|
|
||
|
// first check old-style name
|
||
|
let name = object.name;
|
||
|
|
||
|
if (!name) {
|
||
|
|
||
|
if (!object.meta) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
name = object.meta.name;
|
||
|
|
||
|
if (!name) {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// now check for old-style version
|
||
|
let version = object.version;
|
||
|
|
||
|
if (!version) {
|
||
|
version = object.meta && object.meta.version;
|
||
|
}
|
||
|
|
||
|
// if there's a version then append that
|
||
|
if (version) {
|
||
|
return `${name}@${version}`;
|
||
|
}
|
||
|
|
||
|
return name;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts a languageOptions object to a JSON representation.
|
||
|
* @param {Record<string, any>} languageOptions The options to create a JSON
|
||
|
* representation of.
|
||
|
* @param {string} objectKey The key of the object being converted.
|
||
|
* @returns {Record<string, any>} The JSON representation of the languageOptions.
|
||
|
* @throws {TypeError} If a function is found in the languageOptions.
|
||
|
*/
|
||
|
function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
|
||
|
|
||
|
const result = {};
|
||
|
|
||
|
for (const [key, value] of Object.entries(languageOptions)) {
|
||
|
if (value) {
|
||
|
if (typeof value === "object") {
|
||
|
const name = getObjectId(value);
|
||
|
|
||
|
if (name && hasMethod(value)) {
|
||
|
result[key] = name;
|
||
|
} else {
|
||
|
result[key] = languageOptionsToJSON(value, key);
|
||
|
}
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (typeof value === "function") {
|
||
|
throw new TypeError(`Cannot serialize key "${key}" in ${objectKey}: Function values are not supported.`);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
result[key] = value;
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Exports
|
||
|
//-----------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* Represents a normalized configuration object.
|
||
|
*/
|
||
|
class Config {
|
||
|
|
||
|
/**
|
||
|
* The name to use for the language when serializing to JSON.
|
||
|
* @type {string|undefined}
|
||
|
*/
|
||
|
#languageName;
|
||
|
|
||
|
/**
|
||
|
* The name to use for the processor when serializing to JSON.
|
||
|
* @type {string|undefined}
|
||
|
*/
|
||
|
#processorName;
|
||
|
|
||
|
/**
|
||
|
* Creates a new instance.
|
||
|
* @param {Object} config The configuration object.
|
||
|
*/
|
||
|
constructor(config) {
|
||
|
|
||
|
const { plugins, language, languageOptions, processor, ...otherKeys } = config;
|
||
|
|
||
|
// Validate config object
|
||
|
const schema = new ObjectSchema(flatConfigSchema);
|
||
|
|
||
|
schema.validate(config);
|
||
|
|
||
|
// first, copy all the other keys over
|
||
|
Object.assign(this, otherKeys);
|
||
|
|
||
|
// ensure that a language is specified
|
||
|
if (!language) {
|
||
|
throw new TypeError("Key 'language' is required.");
|
||
|
}
|
||
|
|
||
|
// copy the rest over
|
||
|
this.plugins = plugins;
|
||
|
this.language = language;
|
||
|
|
||
|
// Check language value
|
||
|
const { pluginName: languagePluginName, objectName: localLanguageName } = splitPluginIdentifier(language);
|
||
|
|
||
|
this.#languageName = language;
|
||
|
|
||
|
if (!plugins || !plugins[languagePluginName] || !plugins[languagePluginName].languages || !plugins[languagePluginName].languages[localLanguageName]) {
|
||
|
throw new TypeError(`Key "language": Could not find "${localLanguageName}" in plugin "${languagePluginName}".`);
|
||
|
}
|
||
|
|
||
|
this.language = plugins[languagePluginName].languages[localLanguageName];
|
||
|
|
||
|
if (this.language.defaultLanguageOptions ?? languageOptions) {
|
||
|
this.languageOptions = flatConfigSchema.languageOptions.merge(
|
||
|
this.language.defaultLanguageOptions,
|
||
|
languageOptions
|
||
|
);
|
||
|
} else {
|
||
|
this.languageOptions = {};
|
||
|
}
|
||
|
|
||
|
// Validate language options
|
||
|
try {
|
||
|
this.language.validateLanguageOptions(this.languageOptions);
|
||
|
} catch (error) {
|
||
|
throw new TypeError(`Key "languageOptions": ${error.message}`, { cause: error });
|
||
|
}
|
||
|
|
||
|
// Normalize language options if necessary
|
||
|
if (this.language.normalizeLanguageOptions) {
|
||
|
this.languageOptions = this.language.normalizeLanguageOptions(this.languageOptions);
|
||
|
}
|
||
|
|
||
|
// Check processor value
|
||
|
if (processor) {
|
||
|
this.processor = processor;
|
||
|
|
||
|
if (typeof processor === "string") {
|
||
|
const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor);
|
||
|
|
||
|
this.#processorName = processor;
|
||
|
|
||
|
if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[localProcessorName]) {
|
||
|
throw new TypeError(`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`);
|
||
|
}
|
||
|
|
||
|
this.processor = plugins[pluginName].processors[localProcessorName];
|
||
|
} else if (typeof processor === "object") {
|
||
|
this.#processorName = getObjectId(processor);
|
||
|
this.processor = processor;
|
||
|
} else {
|
||
|
throw new TypeError("Key 'processor' must be a string or an object.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Process the rules
|
||
|
if (this.rules) {
|
||
|
this.#normalizeRulesConfig();
|
||
|
ruleValidator.validate(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts the configuration to a JSON representation.
|
||
|
* @returns {Record<string, any>} The JSON representation of the configuration.
|
||
|
* @throws {Error} If the configuration cannot be serialized.
|
||
|
*/
|
||
|
toJSON() {
|
||
|
|
||
|
if (this.processor && !this.#processorName) {
|
||
|
throw new Error("Could not serialize processor object (missing 'meta' object).");
|
||
|
}
|
||
|
|
||
|
if (!this.#languageName) {
|
||
|
throw new Error("Could not serialize language object (missing 'meta' object).");
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
...this,
|
||
|
plugins: Object.entries(this.plugins).map(([namespace, plugin]) => {
|
||
|
|
||
|
const pluginId = getObjectId(plugin);
|
||
|
|
||
|
if (!pluginId) {
|
||
|
return namespace;
|
||
|
}
|
||
|
|
||
|
return `${namespace}:${pluginId}`;
|
||
|
}),
|
||
|
language: this.#languageName,
|
||
|
languageOptions: languageOptionsToJSON(this.languageOptions),
|
||
|
processor: this.#processorName
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalizes the rules configuration. Ensures that each rule config is
|
||
|
* an array and that the severity is a number. Applies meta.defaultOptions.
|
||
|
* This function modifies `this.rules`.
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
#normalizeRulesConfig() {
|
||
|
for (const [ruleId, originalConfig] of Object.entries(this.rules)) {
|
||
|
|
||
|
// ensure rule config is an array
|
||
|
let ruleConfig = Array.isArray(originalConfig)
|
||
|
? originalConfig
|
||
|
: [originalConfig];
|
||
|
|
||
|
// normalize severity
|
||
|
ruleConfig[0] = severities.get(ruleConfig[0]);
|
||
|
|
||
|
const rule = getRuleFromConfig(ruleId, this);
|
||
|
|
||
|
// apply meta.defaultOptions
|
||
|
const slicedOptions = ruleConfig.slice(1);
|
||
|
const mergedOptions = deepMergeArrays(rule?.meta?.defaultOptions, slicedOptions);
|
||
|
|
||
|
if (mergedOptions.length) {
|
||
|
ruleConfig = [ruleConfig[0], ...mergedOptions];
|
||
|
}
|
||
|
|
||
|
this.rules[ruleId] = ruleConfig;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = { Config };
|