]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/config/flat-config-schema.js
cb8e7961add509f5c4e698f5850d75e7bcd30ddf
[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 * Validates that a value is a valid rule options entry.
131 * @param {any} value The value to check.
132 * @returns {void}
133 * @throws {TypeError} If the value isn't a valid rule options.
134 */
135 function assertIsRuleOptions(value) {
136
137 if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
138 throw new TypeError("Expected a string, number, or array.");
139 }
140 }
141
142 /**
143 * Validates that a value is valid rule severity.
144 * @param {any} value The value to check.
145 * @returns {void}
146 * @throws {TypeError} If the value isn't a valid rule severity.
147 */
148 function assertIsRuleSeverity(value) {
149 const severity = typeof value === "string"
150 ? ruleSeverities.get(value.toLowerCase())
151 : ruleSeverities.get(value);
152
153 if (typeof severity === "undefined") {
154 throw new TypeError("Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2.");
155 }
156 }
157
158 /**
159 * Validates that a given string is the form pluginName/objectName.
160 * @param {string} value The string to check.
161 * @returns {void}
162 * @throws {TypeError} If the string isn't in the correct format.
163 */
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}".`);
167 }
168 }
169
170 /**
171 * Validates that a value is an object.
172 * @param {any} value The value to check.
173 * @returns {void}
174 * @throws {TypeError} If the value isn't an object.
175 */
176 function assertIsObject(value) {
177 if (!isNonNullObject(value)) {
178 throw new TypeError("Expected an object.");
179 }
180 }
181
182 /**
183 * Validates that a value is an object or a string.
184 * @param {any} value The value to check.
185 * @returns {void}
186 * @throws {TypeError} If the value isn't an object or a string.
187 */
188 function assertIsObjectOrString(value) {
189 if ((!value || typeof value !== "object") && typeof value !== "string") {
190 throw new TypeError("Expected an object or string.");
191 }
192 }
193
194 //-----------------------------------------------------------------------------
195 // Low-Level Schemas
196 //-----------------------------------------------------------------------------
197
198 /** @type {ObjectPropertySchema} */
199 const booleanSchema = {
200 merge: "replace",
201 validate: "boolean"
202 };
203
204 /** @type {ObjectPropertySchema} */
205 const deepObjectAssignSchema = {
206 merge(first = {}, second = {}) {
207 return deepMerge(first, second);
208 },
209 validate: "object"
210 };
211
212 //-----------------------------------------------------------------------------
213 // High-Level Schemas
214 //-----------------------------------------------------------------------------
215
216 /** @type {ObjectPropertySchema} */
217 const globalsSchema = {
218 merge: "assign",
219 validate(value) {
220
221 assertIsObject(value);
222
223 for (const key of Object.keys(value)) {
224
225 // avoid hairy edge case
226 if (key === "__proto__") {
227 continue;
228 }
229
230 if (key !== key.trim()) {
231 throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
232 }
233
234 if (!globalVariablesValues.has(value[key])) {
235 throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
236 }
237 }
238 }
239 };
240
241 /** @type {ObjectPropertySchema} */
242 const parserSchema = {
243 merge: "replace",
244 validate(value) {
245 assertIsObjectOrString(value);
246
247 if (typeof value === "object" && typeof value.parse !== "function" && typeof value.parseForESLint !== "function") {
248 throw new TypeError("Expected object to have a parse() or parseForESLint() method.");
249 }
250
251 if (typeof value === "string") {
252 assertIsPluginMemberName(value);
253 }
254 }
255 };
256
257 /** @type {ObjectPropertySchema} */
258 const pluginsSchema = {
259 merge(first = {}, second = {}) {
260 const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
261 const result = {};
262
263 // manually validate that plugins are not redefined
264 for (const key of keys) {
265
266 // avoid hairy edge case
267 if (key === "__proto__") {
268 continue;
269 }
270
271 if (key in first && key in second && first[key] !== second[key]) {
272 throw new TypeError(`Cannot redefine plugin "${key}".`);
273 }
274
275 result[key] = second[key] || first[key];
276 }
277
278 return result;
279 },
280 validate(value) {
281
282 // first check the value to be sure it's an object
283 if (value === null || typeof value !== "object") {
284 throw new TypeError("Expected an object.");
285 }
286
287 // second check the keys to make sure they are objects
288 for (const key of Object.keys(value)) {
289
290 // avoid hairy edge case
291 if (key === "__proto__") {
292 continue;
293 }
294
295 if (value[key] === null || typeof value[key] !== "object") {
296 throw new TypeError(`Key "${key}": Expected an object.`);
297 }
298 }
299 }
300 };
301
302 /** @type {ObjectPropertySchema} */
303 const processorSchema = {
304 merge: "replace",
305 validate(value) {
306 if (typeof value === "string") {
307 assertIsPluginMemberName(value);
308 } else if (value && typeof value === "object") {
309 if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
310 throw new TypeError("Object must have a preprocess() and a postprocess() method.");
311 }
312 } else {
313 throw new TypeError("Expected an object or a string.");
314 }
315 }
316 };
317
318 /** @type {ObjectPropertySchema} */
319 const rulesSchema = {
320 merge(first = {}, second = {}) {
321
322 const result = {
323 ...first,
324 ...second
325 };
326
327 for (const ruleId of Object.keys(result)) {
328
329 // avoid hairy edge case
330 if (ruleId === "__proto__") {
331
332 /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
333 delete result.__proto__;
334 continue;
335 }
336
337 result[ruleId] = normalizeRuleOptions(result[ruleId]);
338
339 /*
340 * If either rule config is missing, then the correct
341 * config is already present and we just need to normalize
342 * the severity.
343 */
344 if (!(ruleId in first) || !(ruleId in second)) {
345 continue;
346 }
347
348 const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
349 const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
350
351 /*
352 * If the second rule config only has a severity (length of 1),
353 * then use that severity and keep the rest of the options from
354 * the first rule config.
355 */
356 if (secondRuleOptions.length === 1) {
357 result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
358 continue;
359 }
360
361 /*
362 * In any other situation, then the second rule config takes
363 * precedence. That means the value at `result[ruleId]` is
364 * already correct and no further work is necessary.
365 */
366 }
367
368 return result;
369 },
370
371 validate(value) {
372 assertIsObject(value);
373
374 let lastRuleId;
375
376 // Performance: One try-catch has less overhead than one per loop iteration
377 try {
378
379 /*
380 * We are not checking the rule schema here because there is no
381 * guarantee that the rule definition is present at this point. Instead
382 * we wait and check the rule schema during the finalization step
383 * of calculating a config.
384 */
385 for (const ruleId of Object.keys(value)) {
386
387 // avoid hairy edge case
388 if (ruleId === "__proto__") {
389 continue;
390 }
391
392 lastRuleId = ruleId;
393
394 const ruleOptions = value[ruleId];
395
396 assertIsRuleOptions(ruleOptions);
397
398 if (Array.isArray(ruleOptions)) {
399 assertIsRuleSeverity(ruleOptions[0]);
400 } else {
401 assertIsRuleSeverity(ruleOptions);
402 }
403 }
404 } catch (error) {
405 error.message = `Key "${lastRuleId}": ${error.message}`;
406 throw error;
407 }
408 }
409 };
410
411 /** @type {ObjectPropertySchema} */
412 const ecmaVersionSchema = {
413 merge: "replace",
414 validate(value) {
415 if (typeof value === "number" || value === "latest") {
416 return;
417 }
418
419 throw new TypeError("Expected a number or \"latest\".");
420 }
421 };
422
423 /** @type {ObjectPropertySchema} */
424 const sourceTypeSchema = {
425 merge: "replace",
426 validate(value) {
427 if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
428 throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
429 }
430 }
431 };
432
433 //-----------------------------------------------------------------------------
434 // Full schema
435 //-----------------------------------------------------------------------------
436
437 exports.flatConfigSchema = {
438 settings: deepObjectAssignSchema,
439 linterOptions: {
440 schema: {
441 noInlineConfig: booleanSchema,
442 reportUnusedDisableDirectives: booleanSchema
443 }
444 },
445 languageOptions: {
446 schema: {
447 ecmaVersion: ecmaVersionSchema,
448 sourceType: sourceTypeSchema,
449 globals: globalsSchema,
450 parser: parserSchema,
451 parserOptions: deepObjectAssignSchema
452 }
453 },
454 processor: processorSchema,
455 plugins: pluginsSchema,
456 rules: rulesSchema
457 };