]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/config/flat-config-schema.js
import 8.41.0 source
[pve-eslint.git] / eslint / lib / config / flat-config-schema.js
1 /**
2 * @fileoverview Flat config schema
3 * @author Nicholas C. Zakas
4 */
5
6 "use strict";
7
8 //-----------------------------------------------------------------------------
9 // Type Definitions
10 //-----------------------------------------------------------------------------
11
12 /**
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.
18 */
19
20 //-----------------------------------------------------------------------------
21 // Helpers
22 //-----------------------------------------------------------------------------
23
24 const ruleSeverities = new Map([
25 [0, 0], ["off", 0],
26 [1, 1], ["warn", 1],
27 [2, 2], ["error", 2]
28 ]);
29
30 const globalVariablesValues = new Set([
31 true, "true", "writable", "writeable",
32 false, "false", "readonly", "readable", null,
33 "off"
34 ]);
35
36 /**
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.
40 */
41 function isNonNullObject(value) {
42 return typeof value === "object" && value !== null;
43 }
44
45 /**
46 * Check if a value is undefined.
47 * @param {any} value The value to check.
48 * @returns {boolean} `true` if the value is undefined.
49 */
50 function isUndefined(value) {
51 return typeof value === "undefined";
52 }
53
54 /**
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.
59 */
60 function deepMerge(first = {}, second = {}) {
61
62 /*
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.
65 */
66 if (Array.isArray(second)) {
67 return second;
68 }
69
70 /*
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
74 * individually.
75 */
76 const result = {
77 ...first,
78 ...second
79 };
80
81 for (const key of Object.keys(second)) {
82
83 // avoid hairy edge case
84 if (key === "__proto__") {
85 continue;
86 }
87
88 const firstValue = first[key];
89 const secondValue = second[key];
90
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) ? [] : {},
97 secondValue
98 );
99 } else if (!isUndefined(secondValue)) {
100 result[key] = secondValue;
101 }
102 }
103 }
104
105 return result;
106
107 }
108
109 /**
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.
114 */
115 function normalizeRuleOptions(ruleOptions) {
116
117 const finalOptions = Array.isArray(ruleOptions)
118 ? ruleOptions.slice(0)
119 : [ruleOptions];
120
121 finalOptions[0] = ruleSeverities.get(finalOptions[0]);
122 return finalOptions;
123 }
124
125 //-----------------------------------------------------------------------------
126 // Assertions
127 //-----------------------------------------------------------------------------
128
129 /**
130 * The error type when a rule's options are configured with an invalid type.
131 */
132 class InvalidRuleOptionsError extends Error {
133
134 /**
135 * @param {string} ruleId Rule name being configured.
136 * @param {any} value The invalid value.
137 */
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 };
142 }
143 }
144
145 /**
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.
149 * @returns {void}
150 * @throws {InvalidRuleOptionsError} If the value isn't a valid rule options.
151 */
152 function assertIsRuleOptions(ruleId, value) {
153 if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
154 throw new InvalidRuleOptionsError(ruleId, value);
155 }
156 }
157
158 /**
159 * The error type when a rule's severity is invalid.
160 */
161 class InvalidRuleSeverityError extends Error {
162
163 /**
164 * @param {string} ruleId Rule name being configured.
165 * @param {any} value The invalid value.
166 */
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 };
171 }
172 }
173
174 /**
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.
178 * @returns {void}
179 * @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity.
180 */
181 function assertIsRuleSeverity(ruleId, value) {
182 const severity = typeof value === "string"
183 ? ruleSeverities.get(value.toLowerCase())
184 : ruleSeverities.get(value);
185
186 if (typeof severity === "undefined") {
187 throw new InvalidRuleSeverityError(ruleId, value);
188 }
189 }
190
191 /**
192 * Validates that a given string is the form pluginName/objectName.
193 * @param {string} value The string to check.
194 * @returns {void}
195 * @throws {TypeError} If the string isn't in the correct format.
196 */
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}".`);
200 }
201 }
202
203 /**
204 * Validates that a value is an object.
205 * @param {any} value The value to check.
206 * @returns {void}
207 * @throws {TypeError} If the value isn't an object.
208 */
209 function assertIsObject(value) {
210 if (!isNonNullObject(value)) {
211 throw new TypeError("Expected an object.");
212 }
213 }
214
215 //-----------------------------------------------------------------------------
216 // Low-Level Schemas
217 //-----------------------------------------------------------------------------
218
219 /** @type {ObjectPropertySchema} */
220 const booleanSchema = {
221 merge: "replace",
222 validate: "boolean"
223 };
224
225 /** @type {ObjectPropertySchema} */
226 const deepObjectAssignSchema = {
227 merge(first = {}, second = {}) {
228 return deepMerge(first, second);
229 },
230 validate: "object"
231 };
232
233 //-----------------------------------------------------------------------------
234 // High-Level Schemas
235 //-----------------------------------------------------------------------------
236
237 /** @type {ObjectPropertySchema} */
238 const globalsSchema = {
239 merge: "assign",
240 validate(value) {
241
242 assertIsObject(value);
243
244 for (const key of Object.keys(value)) {
245
246 // avoid hairy edge case
247 if (key === "__proto__") {
248 continue;
249 }
250
251 if (key !== key.trim()) {
252 throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
253 }
254
255 if (!globalVariablesValues.has(value[key])) {
256 throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
257 }
258 }
259 }
260 };
261
262 /** @type {ObjectPropertySchema} */
263 const parserSchema = {
264 merge: "replace",
265 validate(value) {
266
267 if (!value || typeof value !== "object" ||
268 (typeof value.parse !== "function" && typeof value.parseForESLint !== "function")
269 ) {
270 throw new TypeError("Expected object with parse() or parseForESLint() method.");
271 }
272
273 }
274 };
275
276 /** @type {ObjectPropertySchema} */
277 const pluginsSchema = {
278 merge(first = {}, second = {}) {
279 const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
280 const result = {};
281
282 // manually validate that plugins are not redefined
283 for (const key of keys) {
284
285 // avoid hairy edge case
286 if (key === "__proto__") {
287 continue;
288 }
289
290 if (key in first && key in second && first[key] !== second[key]) {
291 throw new TypeError(`Cannot redefine plugin "${key}".`);
292 }
293
294 result[key] = second[key] || first[key];
295 }
296
297 return result;
298 },
299 validate(value) {
300
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.");
304 }
305
306 // second check the keys to make sure they are objects
307 for (const key of Object.keys(value)) {
308
309 // avoid hairy edge case
310 if (key === "__proto__") {
311 continue;
312 }
313
314 if (value[key] === null || typeof value[key] !== "object") {
315 throw new TypeError(`Key "${key}": Expected an object.`);
316 }
317 }
318 }
319 };
320
321 /** @type {ObjectPropertySchema} */
322 const processorSchema = {
323 merge: "replace",
324 validate(value) {
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.");
330 }
331 } else {
332 throw new TypeError("Expected an object or a string.");
333 }
334 }
335 };
336
337 /** @type {ObjectPropertySchema} */
338 const rulesSchema = {
339 merge(first = {}, second = {}) {
340
341 const result = {
342 ...first,
343 ...second
344 };
345
346 for (const ruleId of Object.keys(result)) {
347
348 // avoid hairy edge case
349 if (ruleId === "__proto__") {
350
351 /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
352 delete result.__proto__;
353 continue;
354 }
355
356 result[ruleId] = normalizeRuleOptions(result[ruleId]);
357
358 /*
359 * If either rule config is missing, then the correct
360 * config is already present and we just need to normalize
361 * the severity.
362 */
363 if (!(ruleId in first) || !(ruleId in second)) {
364 continue;
365 }
366
367 const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
368 const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
369
370 /*
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.
374 */
375 if (secondRuleOptions.length === 1) {
376 result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
377 continue;
378 }
379
380 /*
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.
384 */
385 }
386
387 return result;
388 },
389
390 validate(value) {
391 assertIsObject(value);
392
393 /*
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.
398 */
399 for (const ruleId of Object.keys(value)) {
400
401 // avoid hairy edge case
402 if (ruleId === "__proto__") {
403 continue;
404 }
405
406 const ruleOptions = value[ruleId];
407
408 assertIsRuleOptions(ruleId, ruleOptions);
409
410 if (Array.isArray(ruleOptions)) {
411 assertIsRuleSeverity(ruleId, ruleOptions[0]);
412 } else {
413 assertIsRuleSeverity(ruleId, ruleOptions);
414 }
415 }
416 }
417 };
418
419 /** @type {ObjectPropertySchema} */
420 const ecmaVersionSchema = {
421 merge: "replace",
422 validate(value) {
423 if (typeof value === "number" || value === "latest") {
424 return;
425 }
426
427 throw new TypeError("Expected a number or \"latest\".");
428 }
429 };
430
431 /** @type {ObjectPropertySchema} */
432 const sourceTypeSchema = {
433 merge: "replace",
434 validate(value) {
435 if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
436 throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
437 }
438 }
439 };
440
441 //-----------------------------------------------------------------------------
442 // Full schema
443 //-----------------------------------------------------------------------------
444
445 exports.flatConfigSchema = {
446 settings: deepObjectAssignSchema,
447 linterOptions: {
448 schema: {
449 noInlineConfig: booleanSchema,
450 reportUnusedDisableDirectives: booleanSchema
451 }
452 },
453 languageOptions: {
454 schema: {
455 ecmaVersion: ecmaVersionSchema,
456 sourceType: sourceTypeSchema,
457 globals: globalsSchema,
458 parser: parserSchema,
459 parserOptions: deepObjectAssignSchema
460 }
461 },
462 processor: processorSchema,
463 plugins: pluginsSchema,
464 rules: rulesSchema
465 };