flat-config-schema.js 17 KB

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