validateOptions.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import arrayEqual from './arrayEqual.mjs';
  2. import { isPlainObject } from './validateTypes.mjs';
  3. const IGNORED_OPTIONS = new Set(['severity', 'message', 'url', 'reportDisables', 'disableFix']);
  4. const REPORT_OPTIONS = new Set([
  5. 'reportDescriptionlessDisables',
  6. 'reportInvalidScopeDisables',
  7. 'reportNeedlessDisables',
  8. 'reportUnscopedDisables',
  9. ]);
  10. /** @typedef {import('stylelint').RuleOptions} RuleOptions */
  11. /** @typedef {import('stylelint').RuleOptionsPossible} RuleOptionsPossible */
  12. /**
  13. * @type {import('stylelint').Utils['validateOptions']}
  14. */
  15. export default function validateOptions(result, ruleName, ...optionDescriptions) {
  16. const mustValidate = result.stylelint.config?.validate;
  17. if (!mustValidate) return true;
  18. let noErrors = true;
  19. for (const optionDescription of optionDescriptions) {
  20. validate(optionDescription, ruleName, complain);
  21. }
  22. /**
  23. * @param {string} message
  24. */
  25. function complain(message) {
  26. noErrors = false;
  27. result.warn(message, {
  28. stylelintType: 'invalidOption',
  29. });
  30. result.stylelint.stylelintError = true;
  31. }
  32. return noErrors;
  33. }
  34. /**
  35. * @param {RuleOptions} opts
  36. * @param {string} ruleName
  37. * @param {(message: string) => void} complain
  38. */
  39. function validate({ possible, actual, optional }, ruleName, complain) {
  40. if (actual === false && !REPORT_OPTIONS.has(ruleName)) {
  41. return complain(
  42. `Invalid option value "false" for rule "${ruleName}". Are you trying to disable this rule? If so use "null" instead`,
  43. );
  44. }
  45. // `null` means to turn off a rule.
  46. if (actual === null || arrayEqual(actual, [null])) {
  47. return;
  48. }
  49. const nothingPossible =
  50. possible === undefined ||
  51. possible === null ||
  52. (Array.isArray(possible) && possible.length === 0) ||
  53. (isPlainObject(possible) && Object.keys(possible).length === 0);
  54. if (nothingPossible && actual === true) {
  55. return;
  56. }
  57. if (actual === undefined) {
  58. if (nothingPossible || optional) {
  59. return;
  60. }
  61. complain(`Expected option value for rule "${ruleName}"`);
  62. return;
  63. }
  64. if (nothingPossible) {
  65. if (optional) {
  66. complain(
  67. `Incorrect configuration for rule "${ruleName}". Rule should have "possible" values for options validation`,
  68. );
  69. return;
  70. }
  71. complain(`Unexpected option value ${stringify(actual)} for rule "${ruleName}"`);
  72. return;
  73. }
  74. if (typeof possible === 'function') {
  75. if (!possible(actual)) {
  76. complain(`Invalid option ${stringify(actual)} for rule "${ruleName}"`);
  77. }
  78. return;
  79. }
  80. // If `possible` is an array instead of an object ...
  81. if (Array.isArray(possible)) {
  82. for (const a of [actual].flat()) {
  83. if (isValid(possible, a)) {
  84. continue;
  85. }
  86. complain(`Invalid option value ${stringify(a)} for rule "${ruleName}"`);
  87. }
  88. return;
  89. }
  90. // If actual is NOT an object ...
  91. if (!isPlainObject(actual) || typeof actual !== 'object' || actual === null) {
  92. complain(
  93. `Invalid option value ${stringify(actual)} for rule "${ruleName}": should be an object`,
  94. );
  95. return;
  96. }
  97. for (const [optionName, optionValue] of Object.entries(actual)) {
  98. if (IGNORED_OPTIONS.has(optionName)) {
  99. continue;
  100. }
  101. const possibleValue = possible && possible[optionName];
  102. if (!possibleValue) {
  103. complain(`Invalid option name "${optionName}" for rule "${ruleName}"`);
  104. continue;
  105. }
  106. for (const a of [optionValue].flat()) {
  107. if (isValid(possibleValue, a)) {
  108. continue;
  109. }
  110. complain(`Invalid value ${stringify(a)} for option "${optionName}" of rule "${ruleName}"`);
  111. }
  112. }
  113. }
  114. /**
  115. * @param {RuleOptionsPossible | RuleOptionsPossible[]} possible
  116. * @param {unknown} actual
  117. * @returns {boolean}
  118. */
  119. function isValid(possible, actual) {
  120. for (const possibility of [possible].flat()) {
  121. if (typeof possibility === 'function' && possibility(actual)) {
  122. return true;
  123. }
  124. if (actual === possibility) {
  125. return true;
  126. }
  127. }
  128. return false;
  129. }
  130. /**
  131. * @param {unknown} value
  132. * @returns {string}
  133. */
  134. function stringify(value) {
  135. if (typeof value === 'string') {
  136. return `"${value}"`;
  137. }
  138. return `"${JSON.stringify(value)}"`;
  139. }