rule-validator.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /**
  2. * @fileoverview Rule Validator
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //-----------------------------------------------------------------------------
  7. // Requirements
  8. //-----------------------------------------------------------------------------
  9. const ajvImport = require("../shared/ajv");
  10. const ajv = ajvImport();
  11. const {
  12. parseRuleId,
  13. getRuleFromConfig,
  14. getRuleOptionsSchema
  15. } = require("./flat-config-helpers");
  16. const ruleReplacements = require("../../conf/replacements.json");
  17. //-----------------------------------------------------------------------------
  18. // Helpers
  19. //-----------------------------------------------------------------------------
  20. /**
  21. * Throws a helpful error when a rule cannot be found.
  22. * @param {Object} ruleId The rule identifier.
  23. * @param {string} ruleId.pluginName The ID of the rule to find.
  24. * @param {string} ruleId.ruleName The ID of the rule to find.
  25. * @param {Object} config The config to search in.
  26. * @throws {TypeError} For missing plugin or rule.
  27. * @returns {void}
  28. */
  29. function throwRuleNotFoundError({ pluginName, ruleName }, config) {
  30. const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`;
  31. const errorMessageHeader = `Key "rules": Key "${ruleId}"`;
  32. let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}".`;
  33. // if the plugin exists then we need to check if the rule exists
  34. if (config.plugins && config.plugins[pluginName]) {
  35. const replacementRuleName = ruleReplacements.rules[ruleName];
  36. if (pluginName === "@" && replacementRuleName) {
  37. errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`;
  38. } else {
  39. errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`;
  40. // otherwise, let's see if we can find the rule name elsewhere
  41. for (const [otherPluginName, otherPlugin] of Object.entries(config.plugins)) {
  42. if (otherPlugin.rules && otherPlugin.rules[ruleName]) {
  43. errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`;
  44. break;
  45. }
  46. }
  47. }
  48. // falls through to throw error
  49. }
  50. throw new TypeError(errorMessage);
  51. }
  52. /**
  53. * The error type when a rule has an invalid `meta.schema`.
  54. */
  55. class InvalidRuleOptionsSchemaError extends Error {
  56. /**
  57. * Creates a new instance.
  58. * @param {string} ruleId Id of the rule that has an invalid `meta.schema`.
  59. * @param {Error} processingError Error caught while processing the `meta.schema`.
  60. */
  61. constructor(ruleId, processingError) {
  62. super(
  63. `Error while processing options validation schema of rule '${ruleId}': ${processingError.message}`,
  64. { cause: processingError }
  65. );
  66. this.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA";
  67. }
  68. }
  69. //-----------------------------------------------------------------------------
  70. // Exports
  71. //-----------------------------------------------------------------------------
  72. /**
  73. * Implements validation functionality for the rules portion of a config.
  74. */
  75. class RuleValidator {
  76. /**
  77. * Creates a new instance.
  78. */
  79. constructor() {
  80. /**
  81. * A collection of compiled validators for rules that have already
  82. * been validated.
  83. * @type {WeakMap}
  84. */
  85. this.validators = new WeakMap();
  86. }
  87. /**
  88. * Validates all of the rule configurations in a config against each
  89. * rule's schema.
  90. * @param {Object} config The full config to validate. This object must
  91. * contain both the rules section and the plugins section.
  92. * @returns {void}
  93. * @throws {Error} If a rule's configuration does not match its schema.
  94. */
  95. validate(config) {
  96. if (!config.rules) {
  97. return;
  98. }
  99. for (const [ruleId, ruleOptions] of Object.entries(config.rules)) {
  100. // check for edge case
  101. if (ruleId === "__proto__") {
  102. continue;
  103. }
  104. /*
  105. * If a rule is disabled, we don't do any validation. This allows
  106. * users to safely set any value to 0 or "off" without worrying
  107. * that it will cause a validation error.
  108. *
  109. * Note: ruleOptions is always an array at this point because
  110. * this validation occurs after FlatConfigArray has merged and
  111. * normalized values.
  112. */
  113. if (ruleOptions[0] === 0) {
  114. continue;
  115. }
  116. const rule = getRuleFromConfig(ruleId, config);
  117. if (!rule) {
  118. throwRuleNotFoundError(parseRuleId(ruleId), config);
  119. }
  120. // Precompile and cache validator the first time
  121. if (!this.validators.has(rule)) {
  122. try {
  123. const schema = getRuleOptionsSchema(rule);
  124. if (schema) {
  125. this.validators.set(rule, ajv.compile(schema));
  126. }
  127. } catch (err) {
  128. throw new InvalidRuleOptionsSchemaError(ruleId, err);
  129. }
  130. }
  131. const validateRule = this.validators.get(rule);
  132. if (validateRule) {
  133. validateRule(ruleOptions.slice(1));
  134. if (validateRule.errors) {
  135. throw new Error(`Key "rules": Key "${ruleId}":\n${
  136. validateRule.errors.map(
  137. error => {
  138. if (
  139. error.keyword === "additionalProperties" &&
  140. error.schema === false &&
  141. typeof error.parentSchema?.properties === "object" &&
  142. typeof error.params?.additionalProperty === "string"
  143. ) {
  144. const expectedProperties = Object.keys(error.parentSchema.properties).map(property => `"${property}"`);
  145. return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
  146. }
  147. return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
  148. }
  149. ).join("")
  150. }`);
  151. }
  152. }
  153. }
  154. }
  155. }
  156. exports.RuleValidator = RuleValidator;