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 * The error type when a rule's options are configured with an invalid type.
132 class InvalidRuleOptionsError
extends Error
{
135 * @param {string} ruleId Rule name being configured.
136 * @param {any} value The invalid value.
138 constructor(ruleId
, value
) {
139 super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
140 this.messageTemplate
= "invalid-rule-options";
141 this.messageData
= { ruleId
, value
};
146 * Validates that a value is a valid rule options entry.
147 * @param {string} ruleId Rule name being configured.
148 * @param {any} value The value to check.
150 * @throws {InvalidRuleOptionsError} If the value isn't a valid rule options.
152 function assertIsRuleOptions(ruleId
, value
) {
153 if (typeof value
!== "string" && typeof value
!== "number" && !Array
.isArray(value
)) {
154 throw new InvalidRuleOptionsError(ruleId
, value
);
159 * The error type when a rule's severity is invalid.
161 class InvalidRuleSeverityError
extends Error
{
164 * @param {string} ruleId Rule name being configured.
165 * @param {any} value The invalid value.
167 constructor(ruleId
, value
) {
168 super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
169 this.messageTemplate
= "invalid-rule-severity";
170 this.messageData
= { ruleId
, value
};
175 * Validates that a value is valid rule severity.
176 * @param {string} ruleId Rule name being configured.
177 * @param {any} value The value to check.
179 * @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity.
181 function assertIsRuleSeverity(ruleId
, value
) {
182 const severity
= typeof value
=== "string"
183 ? ruleSeverities
.get(value
.toLowerCase())
184 : ruleSeverities
.get(value
);
186 if (typeof severity
=== "undefined") {
187 throw new InvalidRuleSeverityError(ruleId
, value
);
192 * Validates that a given string is the form pluginName/objectName.
193 * @param {string} value The string to check.
195 * @throws {TypeError} If the string isn't in the correct format.
197 function assertIsPluginMemberName(value
) {
198 if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value
)) {
199 throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
204 * Validates that a value is an object.
205 * @param {any} value The value to check.
207 * @throws {TypeError} If the value isn't an object.
209 function assertIsObject(value
) {
210 if (!isNonNullObject(value
)) {
211 throw new TypeError("Expected an object.");
215 //-----------------------------------------------------------------------------
217 //-----------------------------------------------------------------------------
219 /** @type {ObjectPropertySchema} */
220 const booleanSchema
= {
225 /** @type {ObjectPropertySchema} */
226 const deepObjectAssignSchema
= {
227 merge(first
= {}, second
= {}) {
228 return deepMerge(first
, second
);
233 //-----------------------------------------------------------------------------
234 // High-Level Schemas
235 //-----------------------------------------------------------------------------
237 /** @type {ObjectPropertySchema} */
238 const globalsSchema
= {
242 assertIsObject(value
);
244 for (const key
of Object
.keys(value
)) {
246 // avoid hairy edge case
247 if (key
=== "__proto__") {
251 if (key
!== key
.trim()) {
252 throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
255 if (!globalVariablesValues
.has(value
[key
])) {
256 throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
262 /** @type {ObjectPropertySchema} */
263 const parserSchema
= {
267 if (!value
|| typeof value
!== "object" ||
268 (typeof value
.parse
!== "function" && typeof value
.parseForESLint
!== "function")
270 throw new TypeError("Expected object with parse() or parseForESLint() method.");
276 /** @type {ObjectPropertySchema} */
277 const pluginsSchema
= {
278 merge(first
= {}, second
= {}) {
279 const keys
= new Set([...Object
.keys(first
), ...Object
.keys(second
)]);
282 // manually validate that plugins are not redefined
283 for (const key
of keys
) {
285 // avoid hairy edge case
286 if (key
=== "__proto__") {
290 if (key
in first
&& key
in second
&& first
[key
] !== second
[key
]) {
291 throw new TypeError(`Cannot redefine plugin "${key}".`);
294 result
[key
] = second
[key
] || first
[key
];
301 // first check the value to be sure it's an object
302 if (value
=== null || typeof value
!== "object") {
303 throw new TypeError("Expected an object.");
306 // second check the keys to make sure they are objects
307 for (const key
of Object
.keys(value
)) {
309 // avoid hairy edge case
310 if (key
=== "__proto__") {
314 if (value
[key
] === null || typeof value
[key
] !== "object") {
315 throw new TypeError(`Key "${key}": Expected an object.`);
321 /** @type {ObjectPropertySchema} */
322 const processorSchema
= {
325 if (typeof value
=== "string") {
326 assertIsPluginMemberName(value
);
327 } else if (value
&& typeof value
=== "object") {
328 if (typeof value
.preprocess
!== "function" || typeof value
.postprocess
!== "function") {
329 throw new TypeError("Object must have a preprocess() and a postprocess() method.");
332 throw new TypeError("Expected an object or a string.");
337 /** @type {ObjectPropertySchema} */
338 const rulesSchema
= {
339 merge(first
= {}, second
= {}) {
346 for (const ruleId
of Object
.keys(result
)) {
348 // avoid hairy edge case
349 if (ruleId
=== "__proto__") {
351 /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
352 delete result
.__proto__
;
356 result
[ruleId
] = normalizeRuleOptions(result
[ruleId
]);
359 * If either rule config is missing, then the correct
360 * config is already present and we just need to normalize
363 if (!(ruleId
in first
) || !(ruleId
in second
)) {
367 const firstRuleOptions
= normalizeRuleOptions(first
[ruleId
]);
368 const secondRuleOptions
= normalizeRuleOptions(second
[ruleId
]);
371 * If the second rule config only has a severity (length of 1),
372 * then use that severity and keep the rest of the options from
373 * the first rule config.
375 if (secondRuleOptions
.length
=== 1) {
376 result
[ruleId
] = [secondRuleOptions
[0], ...firstRuleOptions
.slice(1)];
381 * In any other situation, then the second rule config takes
382 * precedence. That means the value at `result[ruleId]` is
383 * already correct and no further work is necessary.
391 assertIsObject(value
);
394 * We are not checking the rule schema here because there is no
395 * guarantee that the rule definition is present at this point. Instead
396 * we wait and check the rule schema during the finalization step
397 * of calculating a config.
399 for (const ruleId
of Object
.keys(value
)) {
401 // avoid hairy edge case
402 if (ruleId
=== "__proto__") {
406 const ruleOptions
= value
[ruleId
];
408 assertIsRuleOptions(ruleId
, ruleOptions
);
410 if (Array
.isArray(ruleOptions
)) {
411 assertIsRuleSeverity(ruleId
, ruleOptions
[0]);
413 assertIsRuleSeverity(ruleId
, ruleOptions
);
419 /** @type {ObjectPropertySchema} */
420 const ecmaVersionSchema
= {
423 if (typeof value
=== "number" || value
=== "latest") {
427 throw new TypeError("Expected a number or \"latest\".");
431 /** @type {ObjectPropertySchema} */
432 const sourceTypeSchema
= {
435 if (typeof value
!== "string" || !/^(?:script|module|commonjs)$/u.test(value
)) {
436 throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
441 //-----------------------------------------------------------------------------
443 //-----------------------------------------------------------------------------
445 exports
.flatConfigSchema
= {
446 settings
: deepObjectAssignSchema
,
449 noInlineConfig
: booleanSchema
,
450 reportUnusedDisableDirectives
: booleanSchema
455 ecmaVersion
: ecmaVersionSchema
,
456 sourceType
: sourceTypeSchema
,
457 globals
: globalsSchema
,
458 parser
: parserSchema
,
459 parserOptions
: deepObjectAssignSchema
462 processor
: processorSchema
,
463 plugins
: pluginsSchema
,