source: imaps-frontend/node_modules/eslint/lib/config/flat-config-schema.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 17.3 KB
RevLine 
[d565449]1/**
2 * @fileoverview Flat config schema
3 * @author Nicholas C. Zakas
4 */
5
6"use strict";
7
8//-----------------------------------------------------------------------------
9// Requirements
10//-----------------------------------------------------------------------------
11
12/*
13 * Note: This can be removed in ESLint v9 because structuredClone is available globally
14 * starting in Node.js v17.
15 */
16const structuredClone = require("@ungap/structured-clone").default;
17const { normalizeSeverityToNumber } = require("../shared/severity");
18
19//-----------------------------------------------------------------------------
20// Type Definitions
21//-----------------------------------------------------------------------------
22
23/**
24 * @typedef ObjectPropertySchema
25 * @property {Function|string} merge The function or name of the function to call
26 * to merge multiple objects with this property.
27 * @property {Function|string} validate The function or name of the function to call
28 * to validate the value of this property.
29 */
30
31//-----------------------------------------------------------------------------
32// Helpers
33//-----------------------------------------------------------------------------
34
35const ruleSeverities = new Map([
36 [0, 0], ["off", 0],
37 [1, 1], ["warn", 1],
38 [2, 2], ["error", 2]
39]);
40
41const globalVariablesValues = new Set([
42 true, "true", "writable", "writeable",
43 false, "false", "readonly", "readable", null,
44 "off"
45]);
46
47/**
48 * Check if a value is a non-null object.
49 * @param {any} value The value to check.
50 * @returns {boolean} `true` if the value is a non-null object.
51 */
52function isNonNullObject(value) {
53 return typeof value === "object" && value !== null;
54}
55
56/**
57 * Check if a value is a non-null non-array object.
58 * @param {any} value The value to check.
59 * @returns {boolean} `true` if the value is a non-null non-array object.
60 */
61function isNonArrayObject(value) {
62 return isNonNullObject(value) && !Array.isArray(value);
63}
64
65/**
66 * Check if a value is undefined.
67 * @param {any} value The value to check.
68 * @returns {boolean} `true` if the value is undefined.
69 */
70function isUndefined(value) {
71 return typeof value === "undefined";
72}
73
74/**
75 * Deeply merges two non-array objects.
76 * @param {Object} first The base object.
77 * @param {Object} second The overrides object.
78 * @param {Map<string, Map<string, Object>>} [mergeMap] Maps the combination of first and second arguments to a merged result.
79 * @returns {Object} An object with properties from both first and second.
80 */
81function deepMerge(first, second, mergeMap = new Map()) {
82
83 let secondMergeMap = mergeMap.get(first);
84
85 if (secondMergeMap) {
86 const result = secondMergeMap.get(second);
87
88 if (result) {
89
90 // If this combination of first and second arguments has been already visited, return the previously created result.
91 return result;
92 }
93 } else {
94 secondMergeMap = new Map();
95 mergeMap.set(first, secondMergeMap);
96 }
97
98 /*
99 * First create a result object where properties from the second object
100 * overwrite properties from the first. This sets up a baseline to use
101 * later rather than needing to inspect and change every property
102 * individually.
103 */
104 const result = {
105 ...first,
106 ...second
107 };
108
109 delete result.__proto__; // eslint-disable-line no-proto -- don't merge own property "__proto__"
110
111 // Store the pending result for this combination of first and second arguments.
112 secondMergeMap.set(second, result);
113
114 for (const key of Object.keys(second)) {
115
116 // avoid hairy edge case
117 if (key === "__proto__" || !Object.prototype.propertyIsEnumerable.call(first, key)) {
118 continue;
119 }
120
121 const firstValue = first[key];
122 const secondValue = second[key];
123
124 if (isNonArrayObject(firstValue) && isNonArrayObject(secondValue)) {
125 result[key] = deepMerge(firstValue, secondValue, mergeMap);
126 } else if (isUndefined(secondValue)) {
127 result[key] = firstValue;
128 }
129 }
130
131 return result;
132
133}
134
135/**
136 * Normalizes the rule options config for a given rule by ensuring that
137 * it is an array and that the first item is 0, 1, or 2.
138 * @param {Array|string|number} ruleOptions The rule options config.
139 * @returns {Array} An array of rule options.
140 */
141function normalizeRuleOptions(ruleOptions) {
142
143 const finalOptions = Array.isArray(ruleOptions)
144 ? ruleOptions.slice(0)
145 : [ruleOptions];
146
147 finalOptions[0] = ruleSeverities.get(finalOptions[0]);
148 return structuredClone(finalOptions);
149}
150
151//-----------------------------------------------------------------------------
152// Assertions
153//-----------------------------------------------------------------------------
154
155/**
156 * The error type when a rule's options are configured with an invalid type.
157 */
158class InvalidRuleOptionsError extends Error {
159
160 /**
161 * @param {string} ruleId Rule name being configured.
162 * @param {any} value The invalid value.
163 */
164 constructor(ruleId, value) {
165 super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
166 this.messageTemplate = "invalid-rule-options";
167 this.messageData = { ruleId, value };
168 }
169}
170
171/**
172 * Validates that a value is a valid rule options entry.
173 * @param {string} ruleId Rule name being configured.
174 * @param {any} value The value to check.
175 * @returns {void}
176 * @throws {InvalidRuleOptionsError} If the value isn't a valid rule options.
177 */
178function assertIsRuleOptions(ruleId, value) {
179 if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
180 throw new InvalidRuleOptionsError(ruleId, value);
181 }
182}
183
184/**
185 * The error type when a rule's severity is invalid.
186 */
187class InvalidRuleSeverityError extends Error {
188
189 /**
190 * @param {string} ruleId Rule name being configured.
191 * @param {any} value The invalid value.
192 */
193 constructor(ruleId, value) {
194 super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
195 this.messageTemplate = "invalid-rule-severity";
196 this.messageData = { ruleId, value };
197 }
198}
199
200/**
201 * Validates that a value is valid rule severity.
202 * @param {string} ruleId Rule name being configured.
203 * @param {any} value The value to check.
204 * @returns {void}
205 * @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity.
206 */
207function assertIsRuleSeverity(ruleId, value) {
208 const severity = ruleSeverities.get(value);
209
210 if (typeof severity === "undefined") {
211 throw new InvalidRuleSeverityError(ruleId, value);
212 }
213}
214
215/**
216 * Validates that a given string is the form pluginName/objectName.
217 * @param {string} value The string to check.
218 * @returns {void}
219 * @throws {TypeError} If the string isn't in the correct format.
220 */
221function assertIsPluginMemberName(value) {
222 if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
223 throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
224 }
225}
226
227/**
228 * Validates that a value is an object.
229 * @param {any} value The value to check.
230 * @returns {void}
231 * @throws {TypeError} If the value isn't an object.
232 */
233function assertIsObject(value) {
234 if (!isNonNullObject(value)) {
235 throw new TypeError("Expected an object.");
236 }
237}
238
239/**
240 * The error type when there's an eslintrc-style options in a flat config.
241 */
242class IncompatibleKeyError extends Error {
243
244 /**
245 * @param {string} key The invalid key.
246 */
247 constructor(key) {
248 super("This appears to be in eslintrc format rather than flat config format.");
249 this.messageTemplate = "eslintrc-incompat";
250 this.messageData = { key };
251 }
252}
253
254/**
255 * The error type when there's an eslintrc-style plugins array found.
256 */
257class IncompatiblePluginsError extends Error {
258
259 /**
260 * Creates a new instance.
261 * @param {Array<string>} plugins The plugins array.
262 */
263 constructor(plugins) {
264 super("This appears to be in eslintrc format (array of strings) rather than flat config format (object).");
265 this.messageTemplate = "eslintrc-plugins";
266 this.messageData = { plugins };
267 }
268}
269
270
271//-----------------------------------------------------------------------------
272// Low-Level Schemas
273//-----------------------------------------------------------------------------
274
275/** @type {ObjectPropertySchema} */
276const booleanSchema = {
277 merge: "replace",
278 validate: "boolean"
279};
280
281const ALLOWED_SEVERITIES = new Set(["error", "warn", "off", 2, 1, 0]);
282
283/** @type {ObjectPropertySchema} */
284const disableDirectiveSeveritySchema = {
285 merge(first, second) {
286 const value = second === void 0 ? first : second;
287
288 if (typeof value === "boolean") {
289 return value ? "warn" : "off";
290 }
291
292 return normalizeSeverityToNumber(value);
293 },
294 validate(value) {
295 if (!(ALLOWED_SEVERITIES.has(value) || typeof value === "boolean")) {
296 throw new TypeError("Expected one of: \"error\", \"warn\", \"off\", 0, 1, 2, or a boolean.");
297 }
298 }
299};
300
301/** @type {ObjectPropertySchema} */
302const deepObjectAssignSchema = {
303 merge(first = {}, second = {}) {
304 return deepMerge(first, second);
305 },
306 validate: "object"
307};
308
309//-----------------------------------------------------------------------------
310// High-Level Schemas
311//-----------------------------------------------------------------------------
312
313/** @type {ObjectPropertySchema} */
314const globalsSchema = {
315 merge: "assign",
316 validate(value) {
317
318 assertIsObject(value);
319
320 for (const key of Object.keys(value)) {
321
322 // avoid hairy edge case
323 if (key === "__proto__") {
324 continue;
325 }
326
327 if (key !== key.trim()) {
328 throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
329 }
330
331 if (!globalVariablesValues.has(value[key])) {
332 throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
333 }
334 }
335 }
336};
337
338/** @type {ObjectPropertySchema} */
339const parserSchema = {
340 merge: "replace",
341 validate(value) {
342
343 if (!value || typeof value !== "object" ||
344 (typeof value.parse !== "function" && typeof value.parseForESLint !== "function")
345 ) {
346 throw new TypeError("Expected object with parse() or parseForESLint() method.");
347 }
348
349 }
350};
351
352/** @type {ObjectPropertySchema} */
353const pluginsSchema = {
354 merge(first = {}, second = {}) {
355 const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
356 const result = {};
357
358 // manually validate that plugins are not redefined
359 for (const key of keys) {
360
361 // avoid hairy edge case
362 if (key === "__proto__") {
363 continue;
364 }
365
366 if (key in first && key in second && first[key] !== second[key]) {
367 throw new TypeError(`Cannot redefine plugin "${key}".`);
368 }
369
370 result[key] = second[key] || first[key];
371 }
372
373 return result;
374 },
375 validate(value) {
376
377 // first check the value to be sure it's an object
378 if (value === null || typeof value !== "object") {
379 throw new TypeError("Expected an object.");
380 }
381
382 // make sure it's not an array, which would mean eslintrc-style is used
383 if (Array.isArray(value)) {
384 throw new IncompatiblePluginsError(value);
385 }
386
387 // second check the keys to make sure they are objects
388 for (const key of Object.keys(value)) {
389
390 // avoid hairy edge case
391 if (key === "__proto__") {
392 continue;
393 }
394
395 if (value[key] === null || typeof value[key] !== "object") {
396 throw new TypeError(`Key "${key}": Expected an object.`);
397 }
398 }
399 }
400};
401
402/** @type {ObjectPropertySchema} */
403const processorSchema = {
404 merge: "replace",
405 validate(value) {
406 if (typeof value === "string") {
407 assertIsPluginMemberName(value);
408 } else if (value && typeof value === "object") {
409 if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
410 throw new TypeError("Object must have a preprocess() and a postprocess() method.");
411 }
412 } else {
413 throw new TypeError("Expected an object or a string.");
414 }
415 }
416};
417
418/** @type {ObjectPropertySchema} */
419const rulesSchema = {
420 merge(first = {}, second = {}) {
421
422 const result = {
423 ...first,
424 ...second
425 };
426
427
428 for (const ruleId of Object.keys(result)) {
429
430 try {
431
432 // avoid hairy edge case
433 if (ruleId === "__proto__") {
434
435 /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
436 delete result.__proto__;
437 continue;
438 }
439
440 result[ruleId] = normalizeRuleOptions(result[ruleId]);
441
442 /*
443 * If either rule config is missing, then the correct
444 * config is already present and we just need to normalize
445 * the severity.
446 */
447 if (!(ruleId in first) || !(ruleId in second)) {
448 continue;
449 }
450
451 const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
452 const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
453
454 /*
455 * If the second rule config only has a severity (length of 1),
456 * then use that severity and keep the rest of the options from
457 * the first rule config.
458 */
459 if (secondRuleOptions.length === 1) {
460 result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
461 continue;
462 }
463
464 /*
465 * In any other situation, then the second rule config takes
466 * precedence. That means the value at `result[ruleId]` is
467 * already correct and no further work is necessary.
468 */
469 } catch (ex) {
470 throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex });
471 }
472
473 }
474
475 return result;
476
477
478 },
479
480 validate(value) {
481 assertIsObject(value);
482
483 /*
484 * We are not checking the rule schema here because there is no
485 * guarantee that the rule definition is present at this point. Instead
486 * we wait and check the rule schema during the finalization step
487 * of calculating a config.
488 */
489 for (const ruleId of Object.keys(value)) {
490
491 // avoid hairy edge case
492 if (ruleId === "__proto__") {
493 continue;
494 }
495
496 const ruleOptions = value[ruleId];
497
498 assertIsRuleOptions(ruleId, ruleOptions);
499
500 if (Array.isArray(ruleOptions)) {
501 assertIsRuleSeverity(ruleId, ruleOptions[0]);
502 } else {
503 assertIsRuleSeverity(ruleId, ruleOptions);
504 }
505 }
506 }
507};
508
509/** @type {ObjectPropertySchema} */
510const ecmaVersionSchema = {
511 merge: "replace",
512 validate(value) {
513 if (typeof value === "number" || value === "latest") {
514 return;
515 }
516
517 throw new TypeError("Expected a number or \"latest\".");
518 }
519};
520
521/** @type {ObjectPropertySchema} */
522const sourceTypeSchema = {
523 merge: "replace",
524 validate(value) {
525 if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
526 throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
527 }
528 }
529};
530
531/**
532 * Creates a schema that always throws an error. Useful for warning
533 * about eslintrc-style keys.
534 * @param {string} key The eslintrc key to create a schema for.
535 * @returns {ObjectPropertySchema} The schema.
536 */
537function createEslintrcErrorSchema(key) {
538 return {
539 merge: "replace",
540 validate() {
541 throw new IncompatibleKeyError(key);
542 }
543 };
544}
545
546const eslintrcKeys = [
547 "env",
548 "extends",
549 "globals",
550 "ignorePatterns",
551 "noInlineConfig",
552 "overrides",
553 "parser",
554 "parserOptions",
555 "reportUnusedDisableDirectives",
556 "root"
557];
558
559//-----------------------------------------------------------------------------
560// Full schema
561//-----------------------------------------------------------------------------
562
563const flatConfigSchema = {
564
565 // eslintrc-style keys that should always error
566 ...Object.fromEntries(eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)])),
567
568 // flat config keys
569 settings: deepObjectAssignSchema,
570 linterOptions: {
571 schema: {
572 noInlineConfig: booleanSchema,
573 reportUnusedDisableDirectives: disableDirectiveSeveritySchema
574 }
575 },
576 languageOptions: {
577 schema: {
578 ecmaVersion: ecmaVersionSchema,
579 sourceType: sourceTypeSchema,
580 globals: globalsSchema,
581 parser: parserSchema,
582 parserOptions: deepObjectAssignSchema
583 }
584 },
585 processor: processorSchema,
586 plugins: pluginsSchema,
587 rules: rulesSchema
588};
589
590//-----------------------------------------------------------------------------
591// Exports
592//-----------------------------------------------------------------------------
593
594module.exports = {
595 flatConfigSchema,
596 assertIsRuleSeverity,
597 assertIsRuleOptions
598};
Note: See TracBrowser for help on using the repository browser.