flat-config-schema.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /**
  2. * @fileoverview Flat config schema
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //-----------------------------------------------------------------------------
  7. // Type Definitions
  8. //-----------------------------------------------------------------------------
  9. /**
  10. * @typedef ObjectPropertySchema
  11. * @property {Function|string} merge The function or name of the function to call
  12. * to merge multiple objects with this property.
  13. * @property {Function|string} validate The function or name of the function to call
  14. * to validate the value of this property.
  15. */
  16. //-----------------------------------------------------------------------------
  17. // Helpers
  18. //-----------------------------------------------------------------------------
  19. const ruleSeverities = new Map([
  20. [0, 0], ["off", 0],
  21. [1, 1], ["warn", 1],
  22. [2, 2], ["error", 2]
  23. ]);
  24. const globalVariablesValues = new Set([
  25. true, "true", "writable", "writeable",
  26. false, "false", "readonly", "readable", null,
  27. "off"
  28. ]);
  29. /**
  30. * Check if a value is a non-null object.
  31. * @param {any} value The value to check.
  32. * @returns {boolean} `true` if the value is a non-null object.
  33. */
  34. function isNonNullObject(value) {
  35. return typeof value === "object" && value !== null;
  36. }
  37. /**
  38. * Check if a value is undefined.
  39. * @param {any} value The value to check.
  40. * @returns {boolean} `true` if the value is undefined.
  41. */
  42. function isUndefined(value) {
  43. return typeof value === "undefined";
  44. }
  45. /**
  46. * Deeply merges two objects.
  47. * @param {Object} first The base object.
  48. * @param {Object} second The overrides object.
  49. * @returns {Object} An object with properties from both first and second.
  50. */
  51. function deepMerge(first = {}, second = {}) {
  52. /*
  53. * If the second value is an array, just return it. We don't merge
  54. * arrays because order matters and we can't know the correct order.
  55. */
  56. if (Array.isArray(second)) {
  57. return second;
  58. }
  59. /*
  60. * First create a result object where properties from the second object
  61. * overwrite properties from the first. This sets up a baseline to use
  62. * later rather than needing to inspect and change every property
  63. * individually.
  64. */
  65. const result = {
  66. ...first,
  67. ...second
  68. };
  69. for (const key of Object.keys(second)) {
  70. // avoid hairy edge case
  71. if (key === "__proto__") {
  72. continue;
  73. }
  74. const firstValue = first[key];
  75. const secondValue = second[key];
  76. if (isNonNullObject(firstValue)) {
  77. result[key] = deepMerge(firstValue, secondValue);
  78. } else if (isUndefined(firstValue)) {
  79. if (isNonNullObject(secondValue)) {
  80. result[key] = deepMerge(
  81. Array.isArray(secondValue) ? [] : {},
  82. secondValue
  83. );
  84. } else if (!isUndefined(secondValue)) {
  85. result[key] = secondValue;
  86. }
  87. }
  88. }
  89. return result;
  90. }
  91. /**
  92. * Normalizes the rule options config for a given rule by ensuring that
  93. * it is an array and that the first item is 0, 1, or 2.
  94. * @param {Array|string|number} ruleOptions The rule options config.
  95. * @returns {Array} An array of rule options.
  96. */
  97. function normalizeRuleOptions(ruleOptions) {
  98. const finalOptions = Array.isArray(ruleOptions)
  99. ? ruleOptions.slice(0)
  100. : [ruleOptions];
  101. finalOptions[0] = ruleSeverities.get(finalOptions[0]);
  102. return finalOptions;
  103. }
  104. //-----------------------------------------------------------------------------
  105. // Assertions
  106. //-----------------------------------------------------------------------------
  107. /**
  108. * Validates that a value is a valid rule options entry.
  109. * @param {any} value The value to check.
  110. * @returns {void}
  111. * @throws {TypeError} If the value isn't a valid rule options.
  112. */
  113. function assertIsRuleOptions(value) {
  114. if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
  115. throw new TypeError("Expected a string, number, or array.");
  116. }
  117. }
  118. /**
  119. * Validates that a value is valid rule severity.
  120. * @param {any} value The value to check.
  121. * @returns {void}
  122. * @throws {TypeError} If the value isn't a valid rule severity.
  123. */
  124. function assertIsRuleSeverity(value) {
  125. const severity = typeof value === "string"
  126. ? ruleSeverities.get(value.toLowerCase())
  127. : ruleSeverities.get(value);
  128. if (typeof severity === "undefined") {
  129. throw new TypeError("Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2.");
  130. }
  131. }
  132. /**
  133. * Validates that a given string is the form pluginName/objectName.
  134. * @param {string} value The string to check.
  135. * @returns {void}
  136. * @throws {TypeError} If the string isn't in the correct format.
  137. */
  138. function assertIsPluginMemberName(value) {
  139. if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
  140. throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
  141. }
  142. }
  143. /**
  144. * Validates that a value is an object.
  145. * @param {any} value The value to check.
  146. * @returns {void}
  147. * @throws {TypeError} If the value isn't an object.
  148. */
  149. function assertIsObject(value) {
  150. if (!isNonNullObject(value)) {
  151. throw new TypeError("Expected an object.");
  152. }
  153. }
  154. /**
  155. * Validates that a value is an object or a string.
  156. * @param {any} value The value to check.
  157. * @returns {void}
  158. * @throws {TypeError} If the value isn't an object or a string.
  159. */
  160. function assertIsObjectOrString(value) {
  161. if ((!value || typeof value !== "object") && typeof value !== "string") {
  162. throw new TypeError("Expected an object or string.");
  163. }
  164. }
  165. //-----------------------------------------------------------------------------
  166. // Low-Level Schemas
  167. //-----------------------------------------------------------------------------
  168. /** @type {ObjectPropertySchema} */
  169. const booleanSchema = {
  170. merge: "replace",
  171. validate: "boolean"
  172. };
  173. /** @type {ObjectPropertySchema} */
  174. const deepObjectAssignSchema = {
  175. merge(first = {}, second = {}) {
  176. return deepMerge(first, second);
  177. },
  178. validate: "object"
  179. };
  180. //-----------------------------------------------------------------------------
  181. // High-Level Schemas
  182. //-----------------------------------------------------------------------------
  183. /** @type {ObjectPropertySchema} */
  184. const globalsSchema = {
  185. merge: "assign",
  186. validate(value) {
  187. assertIsObject(value);
  188. for (const key of Object.keys(value)) {
  189. // avoid hairy edge case
  190. if (key === "__proto__") {
  191. continue;
  192. }
  193. if (key !== key.trim()) {
  194. throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
  195. }
  196. if (!globalVariablesValues.has(value[key])) {
  197. throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
  198. }
  199. }
  200. }
  201. };
  202. /** @type {ObjectPropertySchema} */
  203. const parserSchema = {
  204. merge: "replace",
  205. validate(value) {
  206. assertIsObjectOrString(value);
  207. if (typeof value === "object" && typeof value.parse !== "function" && typeof value.parseForESLint !== "function") {
  208. throw new TypeError("Expected object to have a parse() or parseForESLint() method.");
  209. }
  210. if (typeof value === "string") {
  211. assertIsPluginMemberName(value);
  212. }
  213. }
  214. };
  215. /** @type {ObjectPropertySchema} */
  216. const pluginsSchema = {
  217. merge(first = {}, second = {}) {
  218. const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
  219. const result = {};
  220. // manually validate that plugins are not redefined
  221. for (const key of keys) {
  222. // avoid hairy edge case
  223. if (key === "__proto__") {
  224. continue;
  225. }
  226. if (key in first && key in second && first[key] !== second[key]) {
  227. throw new TypeError(`Cannot redefine plugin "${key}".`);
  228. }
  229. result[key] = second[key] || first[key];
  230. }
  231. return result;
  232. },
  233. validate(value) {
  234. // first check the value to be sure it's an object
  235. if (value === null || typeof value !== "object") {
  236. throw new TypeError("Expected an object.");
  237. }
  238. // second check the keys to make sure they are objects
  239. for (const key of Object.keys(value)) {
  240. // avoid hairy edge case
  241. if (key === "__proto__") {
  242. continue;
  243. }
  244. if (value[key] === null || typeof value[key] !== "object") {
  245. throw new TypeError(`Key "${key}": Expected an object.`);
  246. }
  247. }
  248. }
  249. };
  250. /** @type {ObjectPropertySchema} */
  251. const processorSchema = {
  252. merge: "replace",
  253. validate(value) {
  254. if (typeof value === "string") {
  255. assertIsPluginMemberName(value);
  256. } else if (value && typeof value === "object") {
  257. if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
  258. throw new TypeError("Object must have a preprocess() and a postprocess() method.");
  259. }
  260. } else {
  261. throw new TypeError("Expected an object or a string.");
  262. }
  263. }
  264. };
  265. /** @type {ObjectPropertySchema} */
  266. const rulesSchema = {
  267. merge(first = {}, second = {}) {
  268. const result = {
  269. ...first,
  270. ...second
  271. };
  272. for (const ruleId of Object.keys(result)) {
  273. // avoid hairy edge case
  274. if (ruleId === "__proto__") {
  275. /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
  276. delete result.__proto__;
  277. continue;
  278. }
  279. result[ruleId] = normalizeRuleOptions(result[ruleId]);
  280. /*
  281. * If either rule config is missing, then the correct
  282. * config is already present and we just need to normalize
  283. * the severity.
  284. */
  285. if (!(ruleId in first) || !(ruleId in second)) {
  286. continue;
  287. }
  288. const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
  289. const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
  290. /*
  291. * If the second rule config only has a severity (length of 1),
  292. * then use that severity and keep the rest of the options from
  293. * the first rule config.
  294. */
  295. if (secondRuleOptions.length === 1) {
  296. result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
  297. continue;
  298. }
  299. /*
  300. * In any other situation, then the second rule config takes
  301. * precedence. That means the value at `result[ruleId]` is
  302. * already correct and no further work is necessary.
  303. */
  304. }
  305. return result;
  306. },
  307. validate(value) {
  308. assertIsObject(value);
  309. let lastRuleId;
  310. // Performance: One try-catch has less overhead than one per loop iteration
  311. try {
  312. /*
  313. * We are not checking the rule schema here because there is no
  314. * guarantee that the rule definition is present at this point. Instead
  315. * we wait and check the rule schema during the finalization step
  316. * of calculating a config.
  317. */
  318. for (const ruleId of Object.keys(value)) {
  319. // avoid hairy edge case
  320. if (ruleId === "__proto__") {
  321. continue;
  322. }
  323. lastRuleId = ruleId;
  324. const ruleOptions = value[ruleId];
  325. assertIsRuleOptions(ruleOptions);
  326. if (Array.isArray(ruleOptions)) {
  327. assertIsRuleSeverity(ruleOptions[0]);
  328. } else {
  329. assertIsRuleSeverity(ruleOptions);
  330. }
  331. }
  332. } catch (error) {
  333. error.message = `Key "${lastRuleId}": ${error.message}`;
  334. throw error;
  335. }
  336. }
  337. };
  338. /** @type {ObjectPropertySchema} */
  339. const ecmaVersionSchema = {
  340. merge: "replace",
  341. validate(value) {
  342. if (typeof value === "number" || value === "latest") {
  343. return;
  344. }
  345. throw new TypeError("Expected a number or \"latest\".");
  346. }
  347. };
  348. /** @type {ObjectPropertySchema} */
  349. const sourceTypeSchema = {
  350. merge: "replace",
  351. validate(value) {
  352. if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
  353. throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
  354. }
  355. }
  356. };
  357. //-----------------------------------------------------------------------------
  358. // Full schema
  359. //-----------------------------------------------------------------------------
  360. exports.flatConfigSchema = {
  361. settings: deepObjectAssignSchema,
  362. linterOptions: {
  363. schema: {
  364. noInlineConfig: booleanSchema,
  365. reportUnusedDisableDirectives: booleanSchema
  366. }
  367. },
  368. languageOptions: {
  369. schema: {
  370. ecmaVersion: ecmaVersionSchema,
  371. sourceType: sourceTypeSchema,
  372. globals: globalsSchema,
  373. parser: parserSchema,
  374. parserOptions: deepObjectAssignSchema
  375. }
  376. },
  377. processor: processorSchema,
  378. plugins: pluginsSchema,
  379. rules: rulesSchema
  380. };