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