/** * @fileoverview Flat Config Array * @author Nicholas C. Zakas */ "use strict"; //----------------------------------------------------------------------------- // Requirements //----------------------------------------------------------------------------- const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array"); const { flatConfigSchema } = require("./flat-config-schema"); const { RuleValidator } = require("./rule-validator"); const { defaultConfig } = require("./default-config"); const jsPlugin = require("@eslint/js"); //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** * Fields that are considered metadata and not part of the config object. */ const META_FIELDS = new Set(["name"]); const ruleValidator = new RuleValidator(); /** * 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; } /** * Wraps a config error with details about where the error occurred. * @param {Error} error The original error. * @param {number} originalLength The original length of the config array. * @param {number} baseLength The length of the base config. * @returns {TypeError} The new error with details. */ function wrapConfigErrorWithDetails(error, originalLength, baseLength) { let location = "user-defined"; let configIndex = error.index; /* * A config array is set up in this order: * 1. Base config * 2. Original configs * 3. User-defined configs * 4. CLI-defined configs * * So we need to adjust the index to account for the base config. * * - If the index is less than the base length, it's in the base config * (as specified by `baseConfig` argument to `FlatConfigArray` constructor). * - If the index is greater than the base length but less than the original * length + base length, it's in the original config. The original config * is passed to the `FlatConfigArray` constructor as the first argument. * - Otherwise, it's in the user-defined config, which is loaded from the * config file and merged with any command-line options. */ if (error.index < baseLength) { location = "base"; } else if (error.index < originalLength + baseLength) { location = "original"; configIndex = error.index - baseLength; } else { configIndex = error.index - originalLength - baseLength; } return new TypeError( `${error.message.slice(0, -1)} at ${location} index ${configIndex}.`, { cause: error } ); } const originalBaseConfig = Symbol("originalBaseConfig"); const originalLength = Symbol("originalLength"); const baseLength = Symbol("baseLength"); //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Represents an array containing configuration information for ESLint. */ class FlatConfigArray extends ConfigArray { /** * Creates a new instance. * @param {*[]} configs An array of configuration information. * @param {{basePath: string, shouldIgnore: boolean, baseConfig: FlatConfig}} options The options * to use for the config array instance. */ constructor(configs, { basePath, shouldIgnore = true, baseConfig = defaultConfig } = {}) { super(configs, { basePath, schema: flatConfigSchema }); /** * The original length of the array before any modifications. * @type {number} */ this[originalLength] = this.length; if (baseConfig[Symbol.iterator]) { this.unshift(...baseConfig); } else { this.unshift(baseConfig); } /** * The length of the array after applying the base config. * @type {number} */ this[baseLength] = this.length - this[originalLength]; /** * The base config used to build the config array. * @type {Array} */ this[originalBaseConfig] = baseConfig; Object.defineProperty(this, originalBaseConfig, { writable: false }); /** * Determines if `ignores` fields should be honored. * If true, then all `ignores` fields are honored. * if false, then only `ignores` fields in the baseConfig are honored. * @type {boolean} */ this.shouldIgnore = shouldIgnore; Object.defineProperty(this, "shouldIgnore", { writable: false }); } /** * Normalizes the array by calling the superclass method and catching/rethrowing * any ConfigError exceptions with additional details. * @param {any} [context] The context to use to normalize the array. * @returns {Promise} A promise that resolves when the array is normalized. */ normalize(context) { return super.normalize(context) .catch(error => { if (error.name === "ConfigError") { throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]); } throw error; }); } /** * Normalizes the array by calling the superclass method and catching/rethrowing * any ConfigError exceptions with additional details. * @param {any} [context] The context to use to normalize the array. * @returns {FlatConfigArray} The current instance. * @throws {TypeError} If the config is invalid. */ normalizeSync(context) { try { return super.normalizeSync(context); } catch (error) { if (error.name === "ConfigError") { throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]); } throw error; } } /* eslint-disable class-methods-use-this -- Desired as instance method */ /** * Replaces a config with another config to allow us to put strings * in the config array that will be replaced by objects before * normalization. * @param {Object} config The config to preprocess. * @returns {Object} The preprocessed config. */ [ConfigArraySymbol.preprocessConfig](config) { if (config === "eslint:recommended") { // if we are in a Node.js environment warn the user if (typeof process !== "undefined" && process.emitWarning) { process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config."); } return jsPlugin.configs.recommended; } if (config === "eslint:all") { // if we are in a Node.js environment warn the user if (typeof process !== "undefined" && process.emitWarning) { process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config."); } return jsPlugin.configs.all; } /* * If a config object has `ignores` and no other non-meta fields, then it's an object * for global ignores. If `shouldIgnore` is false, that object shouldn't apply, * so we'll remove its `ignores`. */ if ( !this.shouldIgnore && !this[originalBaseConfig].includes(config) && config.ignores && Object.keys(config).filter(key => !META_FIELDS.has(key)).length === 1 ) { /* eslint-disable-next-line no-unused-vars -- need to strip off other keys */ const { ignores, ...otherKeys } = config; return otherKeys; } return config; } /** * Finalizes the config by replacing plugin references with their objects * and validating rule option schemas. * @param {Object} config The config to finalize. * @returns {Object} The finalized config. * @throws {TypeError} If the config is invalid. */ [ConfigArraySymbol.finalizeConfig](config) { const { plugins, languageOptions, processor } = config; let parserName, processorName; let invalidParser = false, invalidProcessor = false; // Check parser value if (languageOptions && languageOptions.parser) { const { parser } = languageOptions; if (typeof parser === "object") { parserName = getObjectId(parser); if (!parserName) { invalidParser = true; } } else { invalidParser = true; } } // Check processor value if (processor) { if (typeof processor === "string") { const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor); 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}".`); } config.processor = plugins[pluginName].processors[localProcessorName]; } else if (typeof processor === "object") { processorName = getObjectId(processor); if (!processorName) { invalidProcessor = true; } } else { invalidProcessor = true; } } ruleValidator.validate(config); // apply special logic for serialization into JSON /* eslint-disable object-shorthand -- shorthand would change "this" value */ Object.defineProperty(config, "toJSON", { value: function() { if (invalidParser) { throw new Error("Could not serialize parser object (missing 'meta' object)."); } if (invalidProcessor) { throw new Error("Could not serialize processor object (missing 'meta' object)."); } return { ...this, plugins: Object.entries(plugins).map(([namespace, plugin]) => { const pluginId = getObjectId(plugin); if (!pluginId) { return namespace; } return `${namespace}:${pluginId}`; }), languageOptions: { ...languageOptions, parser: parserName }, processor: processorName }; } }); /* eslint-enable object-shorthand -- ok to enable now */ return config; } /* eslint-enable class-methods-use-this -- Desired as instance method */ } exports.FlatConfigArray = FlatConfigArray;