2 * @fileoverview Flat config schema
3 * @author Nicholas C. Zakas
8 //-----------------------------------------------------------------------------
10 //-----------------------------------------------------------------------------
13 * @typedef ObjectPropertySchema
14 * @property {Function|string} merge The function or name of the function to call
15 * to merge multiple objects with this property.
16 * @property {Function|string} validate The function or name of the function to call
17 * to validate the value of this property.
20 //-----------------------------------------------------------------------------
22 //-----------------------------------------------------------------------------
24 const ruleSeverities = new Map([
30 const globalVariablesValues = new Set([
31 true, "true", "writable", "writeable",
32 false, "false", "readonly", "readable", null,
37 * Check if a value is a non-null object.
38 * @param {any} value The value to check.
39 * @returns {boolean} `true` if the value is a non-null object.
41 function isNonNullObject(value) {
42 return typeof value === "object" && value !== null;
46 * Check if a value is undefined.
47 * @param {any} value The value to check.
48 * @returns {boolean} `true` if the value is undefined.
50 function isUndefined(value) {
51 return typeof value === "undefined";
55 * Deeply merges two objects.
56 * @param {Object} first The base object.
57 * @param {Object} second The overrides object.
58 * @returns {Object} An object with properties from both first and second.
60 function deepMerge(first = {}, second = {}) {
63 * If the second value is an array, just return it. We don't merge
64 * arrays because order matters and we can't know the correct order.
66 if (Array.isArray(second)) {
71 * First create a result object where properties from the second object
72 * overwrite properties from the first. This sets up a baseline to use
73 * later rather than needing to inspect and change every property
81 for (const key of Object.keys(second)) {
83 // avoid hairy edge case
84 if (key === "__proto__") {
88 const firstValue = first[key];
89 const secondValue = second[key];
91 if (isNonNullObject(firstValue)) {
92 result[key] = deepMerge(firstValue, secondValue);
93 } else if (isUndefined(firstValue)) {
94 if (isNonNullObject(secondValue)) {
95 result[key] = deepMerge(
96 Array.isArray(secondValue) ? [] : {},
99 } else if (!isUndefined(secondValue)) {
100 result[key] = secondValue;
110 * Normalizes the rule options config for a given rule by ensuring that
111 * it is an array and that the first item is 0, 1, or 2.
112 * @param {Array|string|number} ruleOptions The rule options config.
113 * @returns {Array} An array of rule options.
115 function normalizeRuleOptions(ruleOptions) {
117 const finalOptions = Array.isArray(ruleOptions)
118 ? ruleOptions.slice(0)
121 finalOptions[0] = ruleSeverities.get(finalOptions[0]);
125 //-----------------------------------------------------------------------------
127 //-----------------------------------------------------------------------------
130 * Validates that a value is a valid rule options entry.
131 * @param {any} value The value to check.
133 * @throws {TypeError} If the value isn't a valid rule options.
135 function assertIsRuleOptions(value) {
137 if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
138 throw new TypeError("Expected a string, number, or array.");
143 * Validates that a value is valid rule severity.
144 * @param {any} value The value to check.
146 * @throws {TypeError} If the value isn't a valid rule severity.
148 function assertIsRuleSeverity(value) {
149 const severity = typeof value === "string"
150 ? ruleSeverities.get(value.toLowerCase())
151 : ruleSeverities.get(value);
153 if (typeof severity === "undefined") {
154 throw new TypeError("Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2.");
159 * Validates that a given string is the form pluginName/objectName.
160 * @param {string} value The string to check.
162 * @throws {TypeError} If the string isn't in the correct format.
164 function assertIsPluginMemberName(value) {
165 if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
166 throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
171 * Validates that a value is an object.
172 * @param {any} value The value to check.
174 * @throws {TypeError} If the value isn't an object.
176 function assertIsObject(value) {
177 if (!isNonNullObject(value)) {
178 throw new TypeError("Expected an object.");
183 * Validates that a value is an object or a string.
184 * @param {any} value The value to check.
186 * @throws {TypeError} If the value isn't an object or a string.
188 function assertIsObjectOrString(value) {
189 if ((!value || typeof value !== "object") && typeof value !== "string") {
190 throw new TypeError("Expected an object or string.");
194 //-----------------------------------------------------------------------------
196 //-----------------------------------------------------------------------------
199 /** @type {ObjectPropertySchema} */
200 const numberSchema = {
205 /** @type {ObjectPropertySchema} */
206 const booleanSchema = {
211 /** @type {ObjectPropertySchema} */
212 const deepObjectAssignSchema = {
213 merge(first = {}, second = {}) {
214 return deepMerge(first, second);
219 //-----------------------------------------------------------------------------
220 // High-Level Schemas
221 //-----------------------------------------------------------------------------
223 /** @type {ObjectPropertySchema} */
224 const globalsSchema = {
228 assertIsObject(value);
230 for (const key of Object.keys(value)) {
232 // avoid hairy edge case
233 if (key === "__proto__") {
237 if (key !== key.trim()) {
238 throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
241 if (!globalVariablesValues.has(value[key])) {
242 throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
248 /** @type {ObjectPropertySchema} */
249 const parserSchema = {
252 assertIsObjectOrString(value);
254 if (typeof value === "object" && typeof value.parse !== "function" && typeof value.parseForESLint !== "function") {
255 throw new TypeError("Expected object to have a parse() or parseForESLint() method.");
258 if (typeof value === "string") {
259 assertIsPluginMemberName(value);
264 /** @type {ObjectPropertySchema} */
265 const pluginsSchema = {
266 merge(first = {}, second = {}) {
267 const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
270 // manually validate that plugins are not redefined
271 for (const key of keys) {
273 // avoid hairy edge case
274 if (key === "__proto__") {
278 if (key in first && key in second && first[key] !== second[key]) {
279 throw new TypeError(`Cannot redefine plugin "${key}".`);
282 result[key] = second[key] || first[key];
289 // first check the value to be sure it's an object
290 if (value === null || typeof value !== "object") {
291 throw new TypeError("Expected an object.");
294 // second check the keys to make sure they are objects
295 for (const key of Object.keys(value)) {
297 // avoid hairy edge case
298 if (key === "__proto__") {
302 if (value[key] === null || typeof value[key] !== "object") {
303 throw new TypeError(`Key "${key}": Expected an object.`);
309 /** @type {ObjectPropertySchema} */
310 const processorSchema = {
313 if (typeof value === "string") {
314 assertIsPluginMemberName(value);
315 } else if (value && typeof value === "object") {
316 if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
317 throw new TypeError("Object must have a preprocess() and a postprocess() method.");
320 throw new TypeError("Expected an object or a string.");
325 /** @type {ObjectPropertySchema} */
326 const rulesSchema = {
327 merge(first = {}, second = {}) {
334 for (const ruleId of Object.keys(result)) {
336 // avoid hairy edge case
337 if (ruleId === "__proto__") {
339 /* eslint-disable-next-line no-proto */
340 delete result.__proto__;
344 result[ruleId] = normalizeRuleOptions(result[ruleId]);
347 * If either rule config is missing, then the correct
348 * config is already present and we just need to normalize
351 if (!(ruleId in first) || !(ruleId in second)) {
355 const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
356 const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
359 * If the second rule config only has a severity (length of 1),
360 * then use that severity and keep the rest of the options from
361 * the first rule config.
363 if (secondRuleOptions.length === 1) {
364 result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
369 * In any other situation, then the second rule config takes
370 * precedence. That means the value at `result[ruleId]` is
371 * already correct and no further work is necessary.
379 assertIsObject(value);
383 // Performance: One try-catch has less overhead than one per loop iteration
387 * We are not checking the rule schema here because there is no
388 * guarantee that the rule definition is present at this point. Instead
389 * we wait and check the rule schema during the finalization step
390 * of calculating a config.
392 for (const ruleId of Object.keys(value)) {
394 // avoid hairy edge case
395 if (ruleId === "__proto__") {
401 const ruleOptions = value[ruleId];
403 assertIsRuleOptions(ruleOptions);
405 if (Array.isArray(ruleOptions)) {
406 assertIsRuleSeverity(ruleOptions[0]);
408 assertIsRuleSeverity(ruleOptions);
412 error.message = `Key "${lastRuleId}": ${error.message}`;
418 /** @type {ObjectPropertySchema} */
419 const sourceTypeSchema = {
422 if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
423 throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
428 //-----------------------------------------------------------------------------
430 //-----------------------------------------------------------------------------
432 exports.flatConfigSchema = {
433 settings: deepObjectAssignSchema,
436 noInlineConfig: booleanSchema,
437 reportUnusedDisableDirectives: booleanSchema
442 ecmaVersion: numberSchema,
443 sourceType: sourceTypeSchema,
444 globals: globalsSchema,
445 parser: parserSchema,
446 parserOptions: deepObjectAssignSchema
449 processor: processorSchema,
450 plugins: pluginsSchema,