config-validator.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /**
  2. * @fileoverview Validates configs.
  3. * @author Brandon Mills
  4. */
  5. "use strict";
  6. /* eslint class-methods-use-this: "off" */
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const
  11. util = require("util"),
  12. configSchema = require("../../conf/config-schema"),
  13. BuiltInEnvironments = require("../../conf/environments"),
  14. ConfigOps = require("./config-ops"),
  15. { emitDeprecationWarning } = require("./deprecation-warnings");
  16. const ajv = require("./ajv")();
  17. const ruleValidators = new WeakMap();
  18. const noop = Function.prototype;
  19. //------------------------------------------------------------------------------
  20. // Private
  21. //------------------------------------------------------------------------------
  22. let validateSchema;
  23. const severityMap = {
  24. error: 2,
  25. warn: 1,
  26. off: 0
  27. };
  28. const validated = new WeakSet();
  29. //-----------------------------------------------------------------------------
  30. // Exports
  31. //-----------------------------------------------------------------------------
  32. module.exports = class ConfigValidator {
  33. constructor({ builtInRules = new Map() } = {}) {
  34. this.builtInRules = builtInRules;
  35. }
  36. /**
  37. * Gets a complete options schema for a rule.
  38. * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
  39. * @returns {Object} JSON Schema for the rule's options.
  40. */
  41. getRuleOptionsSchema(rule) {
  42. if (!rule) {
  43. return null;
  44. }
  45. const schema = rule.schema || rule.meta && rule.meta.schema;
  46. // Given a tuple of schemas, insert warning level at the beginning
  47. if (Array.isArray(schema)) {
  48. if (schema.length) {
  49. return {
  50. type: "array",
  51. items: schema,
  52. minItems: 0,
  53. maxItems: schema.length
  54. };
  55. }
  56. return {
  57. type: "array",
  58. minItems: 0,
  59. maxItems: 0
  60. };
  61. }
  62. // Given a full schema, leave it alone
  63. return schema || null;
  64. }
  65. /**
  66. * Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
  67. * @param {options} options The given options for the rule.
  68. * @returns {number|string} The rule's severity value
  69. */
  70. validateRuleSeverity(options) {
  71. const severity = Array.isArray(options) ? options[0] : options;
  72. const normSeverity = typeof severity === "string" ? severityMap[severity.toLowerCase()] : severity;
  73. if (normSeverity === 0 || normSeverity === 1 || normSeverity === 2) {
  74. return normSeverity;
  75. }
  76. throw new Error(`\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '${util.inspect(severity).replace(/'/gu, "\"").replace(/\n/gu, "")}').\n`);
  77. }
  78. /**
  79. * Validates the non-severity options passed to a rule, based on its schema.
  80. * @param {{create: Function}} rule The rule to validate
  81. * @param {Array} localOptions The options for the rule, excluding severity
  82. * @returns {void}
  83. */
  84. validateRuleSchema(rule, localOptions) {
  85. if (!ruleValidators.has(rule)) {
  86. const schema = this.getRuleOptionsSchema(rule);
  87. if (schema) {
  88. ruleValidators.set(rule, ajv.compile(schema));
  89. }
  90. }
  91. const validateRule = ruleValidators.get(rule);
  92. if (validateRule) {
  93. validateRule(localOptions);
  94. if (validateRule.errors) {
  95. throw new Error(validateRule.errors.map(
  96. error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
  97. ).join(""));
  98. }
  99. }
  100. }
  101. /**
  102. * Validates a rule's options against its schema.
  103. * @param {{create: Function}|null} rule The rule that the config is being validated for
  104. * @param {string} ruleId The rule's unique name.
  105. * @param {Array|number} options The given options for the rule.
  106. * @param {string|null} source The name of the configuration source to report in any errors. If null or undefined,
  107. * no source is prepended to the message.
  108. * @returns {void}
  109. */
  110. validateRuleOptions(rule, ruleId, options, source = null) {
  111. try {
  112. const severity = this.validateRuleSeverity(options);
  113. if (severity !== 0) {
  114. this.validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
  115. }
  116. } catch (err) {
  117. const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`;
  118. if (typeof source === "string") {
  119. throw new Error(`${source}:\n\t${enhancedMessage}`);
  120. } else {
  121. throw new Error(enhancedMessage);
  122. }
  123. }
  124. }
  125. /**
  126. * Validates an environment object
  127. * @param {Object} environment The environment config object to validate.
  128. * @param {string} source The name of the configuration source to report in any errors.
  129. * @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded environments.
  130. * @returns {void}
  131. */
  132. validateEnvironment(
  133. environment,
  134. source,
  135. getAdditionalEnv = noop
  136. ) {
  137. // not having an environment is ok
  138. if (!environment) {
  139. return;
  140. }
  141. Object.keys(environment).forEach(id => {
  142. const env = getAdditionalEnv(id) || BuiltInEnvironments.get(id) || null;
  143. if (!env) {
  144. const message = `${source}:\n\tEnvironment key "${id}" is unknown\n`;
  145. throw new Error(message);
  146. }
  147. });
  148. }
  149. /**
  150. * Validates a rules config object
  151. * @param {Object} rulesConfig The rules config object to validate.
  152. * @param {string} source The name of the configuration source to report in any errors.
  153. * @param {function(ruleId:string): Object} getAdditionalRule A map from strings to loaded rules
  154. * @returns {void}
  155. */
  156. validateRules(
  157. rulesConfig,
  158. source,
  159. getAdditionalRule = noop
  160. ) {
  161. if (!rulesConfig) {
  162. return;
  163. }
  164. Object.keys(rulesConfig).forEach(id => {
  165. const rule = getAdditionalRule(id) || this.builtInRules.get(id) || null;
  166. this.validateRuleOptions(rule, id, rulesConfig[id], source);
  167. });
  168. }
  169. /**
  170. * Validates a `globals` section of a config file
  171. * @param {Object} globalsConfig The `globals` section
  172. * @param {string|null} source The name of the configuration source to report in the event of an error.
  173. * @returns {void}
  174. */
  175. validateGlobals(globalsConfig, source = null) {
  176. if (!globalsConfig) {
  177. return;
  178. }
  179. Object.entries(globalsConfig)
  180. .forEach(([configuredGlobal, configuredValue]) => {
  181. try {
  182. ConfigOps.normalizeConfigGlobal(configuredValue);
  183. } catch (err) {
  184. throw new Error(`ESLint configuration of global '${configuredGlobal}' in ${source} is invalid:\n${err.message}`);
  185. }
  186. });
  187. }
  188. /**
  189. * Validate `processor` configuration.
  190. * @param {string|undefined} processorName The processor name.
  191. * @param {string} source The name of config file.
  192. * @param {function(id:string): Processor} getProcessor The getter of defined processors.
  193. * @returns {void}
  194. */
  195. validateProcessor(processorName, source, getProcessor) {
  196. if (processorName && !getProcessor(processorName)) {
  197. throw new Error(`ESLint configuration of processor in '${source}' is invalid: '${processorName}' was not found.`);
  198. }
  199. }
  200. /**
  201. * Formats an array of schema validation errors.
  202. * @param {Array} errors An array of error messages to format.
  203. * @returns {string} Formatted error message
  204. */
  205. formatErrors(errors) {
  206. return errors.map(error => {
  207. if (error.keyword === "additionalProperties") {
  208. const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
  209. return `Unexpected top-level property "${formattedPropertyPath}"`;
  210. }
  211. if (error.keyword === "type") {
  212. const formattedField = error.dataPath.slice(1);
  213. const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
  214. const formattedValue = JSON.stringify(error.data);
  215. return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
  216. }
  217. const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
  218. return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
  219. }).map(message => `\t- ${message}.\n`).join("");
  220. }
  221. /**
  222. * Validates the top level properties of the config object.
  223. * @param {Object} config The config object to validate.
  224. * @param {string} source The name of the configuration source to report in any errors.
  225. * @returns {void}
  226. */
  227. validateConfigSchema(config, source = null) {
  228. validateSchema = validateSchema || ajv.compile(configSchema);
  229. if (!validateSchema(config)) {
  230. throw new Error(`ESLint configuration in ${source} is invalid:\n${this.formatErrors(validateSchema.errors)}`);
  231. }
  232. if (Object.hasOwnProperty.call(config, "ecmaFeatures")) {
  233. emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES");
  234. }
  235. }
  236. /**
  237. * Validates an entire config object.
  238. * @param {Object} config The config object to validate.
  239. * @param {string} source The name of the configuration source to report in any errors.
  240. * @param {function(ruleId:string): Object} [getAdditionalRule] A map from strings to loaded rules.
  241. * @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded envs.
  242. * @returns {void}
  243. */
  244. validate(config, source, getAdditionalRule, getAdditionalEnv) {
  245. this.validateConfigSchema(config, source);
  246. this.validateRules(config.rules, source, getAdditionalRule);
  247. this.validateEnvironment(config.env, source, getAdditionalEnv);
  248. this.validateGlobals(config.globals, source);
  249. for (const override of config.overrides || []) {
  250. this.validateRules(override.rules, source, getAdditionalRule);
  251. this.validateEnvironment(override.env, source, getAdditionalEnv);
  252. this.validateGlobals(config.globals, source);
  253. }
  254. }
  255. /**
  256. * Validate config array object.
  257. * @param {ConfigArray} configArray The config array to validate.
  258. * @returns {void}
  259. */
  260. validateConfigArray(configArray) {
  261. const getPluginEnv = Map.prototype.get.bind(configArray.pluginEnvironments);
  262. const getPluginProcessor = Map.prototype.get.bind(configArray.pluginProcessors);
  263. const getPluginRule = Map.prototype.get.bind(configArray.pluginRules);
  264. // Validate.
  265. for (const element of configArray) {
  266. if (validated.has(element)) {
  267. continue;
  268. }
  269. validated.add(element);
  270. this.validateEnvironment(element.env, element.name, getPluginEnv);
  271. this.validateGlobals(element.globals, element.name);
  272. this.validateProcessor(element.processor, element.name, getPluginProcessor);
  273. this.validateRules(element.rules, element.name, getPluginRule);
  274. }
  275. }
  276. };