flat-config-schema.js 17 KB

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