flat-config-schema.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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. // Low-Level Schemas
  156. //-----------------------------------------------------------------------------
  157. /** @type {ObjectPropertySchema} */
  158. const booleanSchema = {
  159. merge: "replace",
  160. validate: "boolean"
  161. };
  162. /** @type {ObjectPropertySchema} */
  163. const deepObjectAssignSchema = {
  164. merge(first = {}, second = {}) {
  165. return deepMerge(first, second);
  166. },
  167. validate: "object"
  168. };
  169. //-----------------------------------------------------------------------------
  170. // High-Level Schemas
  171. //-----------------------------------------------------------------------------
  172. /** @type {ObjectPropertySchema} */
  173. const globalsSchema = {
  174. merge: "assign",
  175. validate(value) {
  176. assertIsObject(value);
  177. for (const key of Object.keys(value)) {
  178. // avoid hairy edge case
  179. if (key === "__proto__") {
  180. continue;
  181. }
  182. if (key !== key.trim()) {
  183. throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
  184. }
  185. if (!globalVariablesValues.has(value[key])) {
  186. throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
  187. }
  188. }
  189. }
  190. };
  191. /** @type {ObjectPropertySchema} */
  192. const parserSchema = {
  193. merge: "replace",
  194. validate(value) {
  195. if (!value || typeof value !== "object" ||
  196. (typeof value.parse !== "function" && typeof value.parseForESLint !== "function")
  197. ) {
  198. throw new TypeError("Expected object with parse() or parseForESLint() method.");
  199. }
  200. }
  201. };
  202. /** @type {ObjectPropertySchema} */
  203. const pluginsSchema = {
  204. merge(first = {}, second = {}) {
  205. const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
  206. const result = {};
  207. // manually validate that plugins are not redefined
  208. for (const key of keys) {
  209. // avoid hairy edge case
  210. if (key === "__proto__") {
  211. continue;
  212. }
  213. if (key in first && key in second && first[key] !== second[key]) {
  214. throw new TypeError(`Cannot redefine plugin "${key}".`);
  215. }
  216. result[key] = second[key] || first[key];
  217. }
  218. return result;
  219. },
  220. validate(value) {
  221. // first check the value to be sure it's an object
  222. if (value === null || typeof value !== "object") {
  223. throw new TypeError("Expected an object.");
  224. }
  225. // second check the keys to make sure they are objects
  226. for (const key of Object.keys(value)) {
  227. // avoid hairy edge case
  228. if (key === "__proto__") {
  229. continue;
  230. }
  231. if (value[key] === null || typeof value[key] !== "object") {
  232. throw new TypeError(`Key "${key}": Expected an object.`);
  233. }
  234. }
  235. }
  236. };
  237. /** @type {ObjectPropertySchema} */
  238. const processorSchema = {
  239. merge: "replace",
  240. validate(value) {
  241. if (typeof value === "string") {
  242. assertIsPluginMemberName(value);
  243. } else if (value && typeof value === "object") {
  244. if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
  245. throw new TypeError("Object must have a preprocess() and a postprocess() method.");
  246. }
  247. } else {
  248. throw new TypeError("Expected an object or a string.");
  249. }
  250. }
  251. };
  252. /** @type {ObjectPropertySchema} */
  253. const rulesSchema = {
  254. merge(first = {}, second = {}) {
  255. const result = {
  256. ...first,
  257. ...second
  258. };
  259. for (const ruleId of Object.keys(result)) {
  260. // avoid hairy edge case
  261. if (ruleId === "__proto__") {
  262. /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
  263. delete result.__proto__;
  264. continue;
  265. }
  266. result[ruleId] = normalizeRuleOptions(result[ruleId]);
  267. /*
  268. * If either rule config is missing, then the correct
  269. * config is already present and we just need to normalize
  270. * the severity.
  271. */
  272. if (!(ruleId in first) || !(ruleId in second)) {
  273. continue;
  274. }
  275. const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
  276. const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
  277. /*
  278. * If the second rule config only has a severity (length of 1),
  279. * then use that severity and keep the rest of the options from
  280. * the first rule config.
  281. */
  282. if (secondRuleOptions.length === 1) {
  283. result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
  284. continue;
  285. }
  286. /*
  287. * In any other situation, then the second rule config takes
  288. * precedence. That means the value at `result[ruleId]` is
  289. * already correct and no further work is necessary.
  290. */
  291. }
  292. return result;
  293. },
  294. validate(value) {
  295. assertIsObject(value);
  296. let lastRuleId;
  297. // Performance: One try-catch has less overhead than one per loop iteration
  298. try {
  299. /*
  300. * We are not checking the rule schema here because there is no
  301. * guarantee that the rule definition is present at this point. Instead
  302. * we wait and check the rule schema during the finalization step
  303. * of calculating a config.
  304. */
  305. for (const ruleId of Object.keys(value)) {
  306. // avoid hairy edge case
  307. if (ruleId === "__proto__") {
  308. continue;
  309. }
  310. lastRuleId = ruleId;
  311. const ruleOptions = value[ruleId];
  312. assertIsRuleOptions(ruleOptions);
  313. if (Array.isArray(ruleOptions)) {
  314. assertIsRuleSeverity(ruleOptions[0]);
  315. } else {
  316. assertIsRuleSeverity(ruleOptions);
  317. }
  318. }
  319. } catch (error) {
  320. error.message = `Key "${lastRuleId}": ${error.message}`;
  321. throw error;
  322. }
  323. }
  324. };
  325. /** @type {ObjectPropertySchema} */
  326. const ecmaVersionSchema = {
  327. merge: "replace",
  328. validate(value) {
  329. if (typeof value === "number" || value === "latest") {
  330. return;
  331. }
  332. throw new TypeError("Expected a number or \"latest\".");
  333. }
  334. };
  335. /** @type {ObjectPropertySchema} */
  336. const sourceTypeSchema = {
  337. merge: "replace",
  338. validate(value) {
  339. if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
  340. throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
  341. }
  342. }
  343. };
  344. //-----------------------------------------------------------------------------
  345. // Full schema
  346. //-----------------------------------------------------------------------------
  347. exports.flatConfigSchema = {
  348. settings: deepObjectAssignSchema,
  349. linterOptions: {
  350. schema: {
  351. noInlineConfig: booleanSchema,
  352. reportUnusedDisableDirectives: booleanSchema
  353. }
  354. },
  355. languageOptions: {
  356. schema: {
  357. ecmaVersion: ecmaVersionSchema,
  358. sourceType: sourceTypeSchema,
  359. globals: globalsSchema,
  360. parser: parserSchema,
  361. parserOptions: deepObjectAssignSchema
  362. }
  363. },
  364. processor: processorSchema,
  365. plugins: pluginsSchema,
  366. rules: rulesSchema
  367. };