|
- /**
- * @fileoverview Flat config schema
- * @author Nicholas C. Zakas
- */
-
- "use strict";
-
- //-----------------------------------------------------------------------------
- // Type Definitions
- //-----------------------------------------------------------------------------
-
- /**
- * @typedef ObjectPropertySchema
- * @property {Function|string} merge The function or name of the function to call
- * to merge multiple objects with this property.
- * @property {Function|string} validate The function or name of the function to call
- * to validate the value of this property.
- */
-
- //-----------------------------------------------------------------------------
- // Helpers
- //-----------------------------------------------------------------------------
-
- const ruleSeverities = new Map([
- [0, 0], ["off", 0],
- [1, 1], ["warn", 1],
- [2, 2], ["error", 2]
- ]);
-
- const globalVariablesValues = new Set([
- true, "true", "writable", "writeable",
- false, "false", "readonly", "readable", null,
- "off"
- ]);
-
- /**
- * Check if a value is a non-null object.
- * @param {any} value The value to check.
- * @returns {boolean} `true` if the value is a non-null object.
- */
- function isNonNullObject(value) {
- return typeof value === "object" && value !== null;
- }
-
- /**
- * Check if a value is undefined.
- * @param {any} value The value to check.
- * @returns {boolean} `true` if the value is undefined.
- */
- function isUndefined(value) {
- return typeof value === "undefined";
- }
-
- /**
- * Deeply merges two objects.
- * @param {Object} first The base object.
- * @param {Object} second The overrides object.
- * @returns {Object} An object with properties from both first and second.
- */
- function deepMerge(first = {}, second = {}) {
-
- /*
- * If the second value is an array, just return it. We don't merge
- * arrays because order matters and we can't know the correct order.
- */
- if (Array.isArray(second)) {
- return second;
- }
-
- /*
- * First create a result object where properties from the second object
- * overwrite properties from the first. This sets up a baseline to use
- * later rather than needing to inspect and change every property
- * individually.
- */
- const result = {
- ...first,
- ...second
- };
-
- for (const key of Object.keys(second)) {
-
- // avoid hairy edge case
- if (key === "__proto__") {
- continue;
- }
-
- const firstValue = first[key];
- const secondValue = second[key];
-
- if (isNonNullObject(firstValue)) {
- result[key] = deepMerge(firstValue, secondValue);
- } else if (isUndefined(firstValue)) {
- if (isNonNullObject(secondValue)) {
- result[key] = deepMerge(
- Array.isArray(secondValue) ? [] : {},
- secondValue
- );
- } else if (!isUndefined(secondValue)) {
- result[key] = secondValue;
- }
- }
- }
-
- return result;
-
- }
-
- /**
- * Normalizes the rule options config for a given rule by ensuring that
- * it is an array and that the first item is 0, 1, or 2.
- * @param {Array|string|number} ruleOptions The rule options config.
- * @returns {Array} An array of rule options.
- */
- function normalizeRuleOptions(ruleOptions) {
-
- const finalOptions = Array.isArray(ruleOptions)
- ? ruleOptions.slice(0)
- : [ruleOptions];
-
- finalOptions[0] = ruleSeverities.get(finalOptions[0]);
- return finalOptions;
- }
-
- //-----------------------------------------------------------------------------
- // Assertions
- //-----------------------------------------------------------------------------
-
- /**
- * Validates that a value is a valid rule options entry.
- * @param {any} value The value to check.
- * @returns {void}
- * @throws {TypeError} If the value isn't a valid rule options.
- */
- function assertIsRuleOptions(value) {
-
- if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
- throw new TypeError("Expected a string, number, or array.");
- }
- }
-
- /**
- * Validates that a value is valid rule severity.
- * @param {any} value The value to check.
- * @returns {void}
- * @throws {TypeError} If the value isn't a valid rule severity.
- */
- function assertIsRuleSeverity(value) {
- const severity = typeof value === "string"
- ? ruleSeverities.get(value.toLowerCase())
- : ruleSeverities.get(value);
-
- if (typeof severity === "undefined") {
- throw new TypeError("Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2.");
- }
- }
-
- /**
- * Validates that a given string is the form pluginName/objectName.
- * @param {string} value The string to check.
- * @returns {void}
- * @throws {TypeError} If the string isn't in the correct format.
- */
- function assertIsPluginMemberName(value) {
- if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
- throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
- }
- }
-
- /**
- * Validates that a value is an object.
- * @param {any} value The value to check.
- * @returns {void}
- * @throws {TypeError} If the value isn't an object.
- */
- function assertIsObject(value) {
- if (!isNonNullObject(value)) {
- throw new TypeError("Expected an object.");
- }
- }
-
- //-----------------------------------------------------------------------------
- // Low-Level Schemas
- //-----------------------------------------------------------------------------
-
- /** @type {ObjectPropertySchema} */
- const booleanSchema = {
- merge: "replace",
- validate: "boolean"
- };
-
- /** @type {ObjectPropertySchema} */
- const deepObjectAssignSchema = {
- merge(first = {}, second = {}) {
- return deepMerge(first, second);
- },
- validate: "object"
- };
-
- //-----------------------------------------------------------------------------
- // High-Level Schemas
- //-----------------------------------------------------------------------------
-
- /** @type {ObjectPropertySchema} */
- const globalsSchema = {
- merge: "assign",
- validate(value) {
-
- assertIsObject(value);
-
- for (const key of Object.keys(value)) {
-
- // avoid hairy edge case
- if (key === "__proto__") {
- continue;
- }
-
- if (key !== key.trim()) {
- throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
- }
-
- if (!globalVariablesValues.has(value[key])) {
- throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
- }
- }
- }
- };
-
- /** @type {ObjectPropertySchema} */
- const parserSchema = {
- merge: "replace",
- validate(value) {
-
- if (!value || typeof value !== "object" ||
- (typeof value.parse !== "function" && typeof value.parseForESLint !== "function")
- ) {
- throw new TypeError("Expected object with parse() or parseForESLint() method.");
- }
-
- }
- };
-
- /** @type {ObjectPropertySchema} */
- const pluginsSchema = {
- merge(first = {}, second = {}) {
- const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
- const result = {};
-
- // manually validate that plugins are not redefined
- for (const key of keys) {
-
- // avoid hairy edge case
- if (key === "__proto__") {
- continue;
- }
-
- if (key in first && key in second && first[key] !== second[key]) {
- throw new TypeError(`Cannot redefine plugin "${key}".`);
- }
-
- result[key] = second[key] || first[key];
- }
-
- return result;
- },
- validate(value) {
-
- // first check the value to be sure it's an object
- if (value === null || typeof value !== "object") {
- throw new TypeError("Expected an object.");
- }
-
- // second check the keys to make sure they are objects
- for (const key of Object.keys(value)) {
-
- // avoid hairy edge case
- if (key === "__proto__") {
- continue;
- }
-
- if (value[key] === null || typeof value[key] !== "object") {
- throw new TypeError(`Key "${key}": Expected an object.`);
- }
- }
- }
- };
-
- /** @type {ObjectPropertySchema} */
- const processorSchema = {
- merge: "replace",
- validate(value) {
- if (typeof value === "string") {
- assertIsPluginMemberName(value);
- } else if (value && typeof value === "object") {
- if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
- throw new TypeError("Object must have a preprocess() and a postprocess() method.");
- }
- } else {
- throw new TypeError("Expected an object or a string.");
- }
- }
- };
-
- /** @type {ObjectPropertySchema} */
- const rulesSchema = {
- merge(first = {}, second = {}) {
-
- const result = {
- ...first,
- ...second
- };
-
- for (const ruleId of Object.keys(result)) {
-
- // avoid hairy edge case
- if (ruleId === "__proto__") {
-
- /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
- delete result.__proto__;
- continue;
- }
-
- result[ruleId] = normalizeRuleOptions(result[ruleId]);
-
- /*
- * If either rule config is missing, then the correct
- * config is already present and we just need to normalize
- * the severity.
- */
- if (!(ruleId in first) || !(ruleId in second)) {
- continue;
- }
-
- const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
- const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
-
- /*
- * If the second rule config only has a severity (length of 1),
- * then use that severity and keep the rest of the options from
- * the first rule config.
- */
- if (secondRuleOptions.length === 1) {
- result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
- continue;
- }
-
- /*
- * In any other situation, then the second rule config takes
- * precedence. That means the value at `result[ruleId]` is
- * already correct and no further work is necessary.
- */
- }
-
- return result;
- },
-
- validate(value) {
- assertIsObject(value);
-
- let lastRuleId;
-
- // Performance: One try-catch has less overhead than one per loop iteration
- try {
-
- /*
- * We are not checking the rule schema here because there is no
- * guarantee that the rule definition is present at this point. Instead
- * we wait and check the rule schema during the finalization step
- * of calculating a config.
- */
- for (const ruleId of Object.keys(value)) {
-
- // avoid hairy edge case
- if (ruleId === "__proto__") {
- continue;
- }
-
- lastRuleId = ruleId;
-
- const ruleOptions = value[ruleId];
-
- assertIsRuleOptions(ruleOptions);
-
- if (Array.isArray(ruleOptions)) {
- assertIsRuleSeverity(ruleOptions[0]);
- } else {
- assertIsRuleSeverity(ruleOptions);
- }
- }
- } catch (error) {
- error.message = `Key "${lastRuleId}": ${error.message}`;
- throw error;
- }
- }
- };
-
- /** @type {ObjectPropertySchema} */
- const ecmaVersionSchema = {
- merge: "replace",
- validate(value) {
- if (typeof value === "number" || value === "latest") {
- return;
- }
-
- throw new TypeError("Expected a number or \"latest\".");
- }
- };
-
- /** @type {ObjectPropertySchema} */
- const sourceTypeSchema = {
- merge: "replace",
- validate(value) {
- if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
- throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
- }
- }
- };
-
- //-----------------------------------------------------------------------------
- // Full schema
- //-----------------------------------------------------------------------------
-
- exports.flatConfigSchema = {
- settings: deepObjectAssignSchema,
- linterOptions: {
- schema: {
- noInlineConfig: booleanSchema,
- reportUnusedDisableDirectives: booleanSchema
- }
- },
- languageOptions: {
- schema: {
- ecmaVersion: ecmaVersionSchema,
- sourceType: sourceTypeSchema,
- globals: globalsSchema,
- parser: parserSchema,
- parserOptions: deepObjectAssignSchema
- }
- },
- processor: processorSchema,
- plugins: pluginsSchema,
- rules: rulesSchema
- };
|