You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
325 lines
12 KiB
325 lines
12 KiB
/** |
|
* @fileoverview Validates configs. |
|
* @author Brandon Mills |
|
*/ |
|
|
|
/* eslint class-methods-use-this: "off" */ |
|
|
|
//------------------------------------------------------------------------------ |
|
// Requirements |
|
//------------------------------------------------------------------------------ |
|
|
|
import util from "util"; |
|
import * as ConfigOps from "./config-ops.js"; |
|
import { emitDeprecationWarning } from "./deprecation-warnings.js"; |
|
import ajvOrig from "./ajv.js"; |
|
import configSchema from "../../conf/config-schema.js"; |
|
import BuiltInEnvironments from "../../conf/environments.js"; |
|
|
|
const ajv = ajvOrig(); |
|
|
|
const ruleValidators = new WeakMap(); |
|
const noop = Function.prototype; |
|
|
|
//------------------------------------------------------------------------------ |
|
// Private |
|
//------------------------------------------------------------------------------ |
|
let validateSchema; |
|
const severityMap = { |
|
error: 2, |
|
warn: 1, |
|
off: 0 |
|
}; |
|
|
|
const validated = new WeakSet(); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Exports |
|
//----------------------------------------------------------------------------- |
|
|
|
export default class ConfigValidator { |
|
constructor({ builtInRules = new Map() } = {}) { |
|
this.builtInRules = builtInRules; |
|
} |
|
|
|
/** |
|
* Gets a complete options schema for a rule. |
|
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object |
|
* @returns {Object} JSON Schema for the rule's options. |
|
*/ |
|
getRuleOptionsSchema(rule) { |
|
if (!rule) { |
|
return null; |
|
} |
|
|
|
const schema = rule.schema || rule.meta && rule.meta.schema; |
|
|
|
// Given a tuple of schemas, insert warning level at the beginning |
|
if (Array.isArray(schema)) { |
|
if (schema.length) { |
|
return { |
|
type: "array", |
|
items: schema, |
|
minItems: 0, |
|
maxItems: schema.length |
|
}; |
|
} |
|
return { |
|
type: "array", |
|
minItems: 0, |
|
maxItems: 0 |
|
}; |
|
|
|
} |
|
|
|
// Given a full schema, leave it alone |
|
return schema || null; |
|
} |
|
|
|
/** |
|
* Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid. |
|
* @param {options} options The given options for the rule. |
|
* @returns {number|string} The rule's severity value |
|
*/ |
|
validateRuleSeverity(options) { |
|
const severity = Array.isArray(options) ? options[0] : options; |
|
const normSeverity = typeof severity === "string" ? severityMap[severity.toLowerCase()] : severity; |
|
|
|
if (normSeverity === 0 || normSeverity === 1 || normSeverity === 2) { |
|
return normSeverity; |
|
} |
|
|
|
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`); |
|
|
|
} |
|
|
|
/** |
|
* Validates the non-severity options passed to a rule, based on its schema. |
|
* @param {{create: Function}} rule The rule to validate |
|
* @param {Array} localOptions The options for the rule, excluding severity |
|
* @returns {void} |
|
*/ |
|
validateRuleSchema(rule, localOptions) { |
|
if (!ruleValidators.has(rule)) { |
|
const schema = this.getRuleOptionsSchema(rule); |
|
|
|
if (schema) { |
|
ruleValidators.set(rule, ajv.compile(schema)); |
|
} |
|
} |
|
|
|
const validateRule = ruleValidators.get(rule); |
|
|
|
if (validateRule) { |
|
validateRule(localOptions); |
|
if (validateRule.errors) { |
|
throw new Error(validateRule.errors.map( |
|
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n` |
|
).join("")); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Validates a rule's options against its schema. |
|
* @param {{create: Function}|null} rule The rule that the config is being validated for |
|
* @param {string} ruleId The rule's unique name. |
|
* @param {Array|number} options The given options for the rule. |
|
* @param {string|null} source The name of the configuration source to report in any errors. If null or undefined, |
|
* no source is prepended to the message. |
|
* @returns {void} |
|
*/ |
|
validateRuleOptions(rule, ruleId, options, source = null) { |
|
try { |
|
const severity = this.validateRuleSeverity(options); |
|
|
|
if (severity !== 0) { |
|
this.validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []); |
|
} |
|
} catch (err) { |
|
const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`; |
|
|
|
if (typeof source === "string") { |
|
throw new Error(`${source}:\n\t${enhancedMessage}`); |
|
} else { |
|
throw new Error(enhancedMessage); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Validates an environment object |
|
* @param {Object} environment The environment config object to validate. |
|
* @param {string} source The name of the configuration source to report in any errors. |
|
* @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded environments. |
|
* @returns {void} |
|
*/ |
|
validateEnvironment( |
|
environment, |
|
source, |
|
getAdditionalEnv = noop |
|
) { |
|
|
|
// not having an environment is ok |
|
if (!environment) { |
|
return; |
|
} |
|
|
|
Object.keys(environment).forEach(id => { |
|
const env = getAdditionalEnv(id) || BuiltInEnvironments.get(id) || null; |
|
|
|
if (!env) { |
|
const message = `${source}:\n\tEnvironment key "${id}" is unknown\n`; |
|
|
|
throw new Error(message); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Validates a rules config object |
|
* @param {Object} rulesConfig The rules config object to validate. |
|
* @param {string} source The name of the configuration source to report in any errors. |
|
* @param {function(ruleId:string): Object} getAdditionalRule A map from strings to loaded rules |
|
* @returns {void} |
|
*/ |
|
validateRules( |
|
rulesConfig, |
|
source, |
|
getAdditionalRule = noop |
|
) { |
|
if (!rulesConfig) { |
|
return; |
|
} |
|
|
|
Object.keys(rulesConfig).forEach(id => { |
|
const rule = getAdditionalRule(id) || this.builtInRules.get(id) || null; |
|
|
|
this.validateRuleOptions(rule, id, rulesConfig[id], source); |
|
}); |
|
} |
|
|
|
/** |
|
* Validates a `globals` section of a config file |
|
* @param {Object} globalsConfig The `globals` section |
|
* @param {string|null} source The name of the configuration source to report in the event of an error. |
|
* @returns {void} |
|
*/ |
|
validateGlobals(globalsConfig, source = null) { |
|
if (!globalsConfig) { |
|
return; |
|
} |
|
|
|
Object.entries(globalsConfig) |
|
.forEach(([configuredGlobal, configuredValue]) => { |
|
try { |
|
ConfigOps.normalizeConfigGlobal(configuredValue); |
|
} catch (err) { |
|
throw new Error(`ESLint configuration of global '${configuredGlobal}' in ${source} is invalid:\n${err.message}`); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Validate `processor` configuration. |
|
* @param {string|undefined} processorName The processor name. |
|
* @param {string} source The name of config file. |
|
* @param {function(id:string): Processor} getProcessor The getter of defined processors. |
|
* @returns {void} |
|
*/ |
|
validateProcessor(processorName, source, getProcessor) { |
|
if (processorName && !getProcessor(processorName)) { |
|
throw new Error(`ESLint configuration of processor in '${source}' is invalid: '${processorName}' was not found.`); |
|
} |
|
} |
|
|
|
/** |
|
* Formats an array of schema validation errors. |
|
* @param {Array} errors An array of error messages to format. |
|
* @returns {string} Formatted error message |
|
*/ |
|
formatErrors(errors) { |
|
return errors.map(error => { |
|
if (error.keyword === "additionalProperties") { |
|
const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty; |
|
|
|
return `Unexpected top-level property "${formattedPropertyPath}"`; |
|
} |
|
if (error.keyword === "type") { |
|
const formattedField = error.dataPath.slice(1); |
|
const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema; |
|
const formattedValue = JSON.stringify(error.data); |
|
|
|
return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`; |
|
} |
|
|
|
const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath; |
|
|
|
return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`; |
|
}).map(message => `\t- ${message}.\n`).join(""); |
|
} |
|
|
|
/** |
|
* Validates the top level properties of the config object. |
|
* @param {Object} config The config object to validate. |
|
* @param {string} source The name of the configuration source to report in any errors. |
|
* @returns {void} |
|
*/ |
|
validateConfigSchema(config, source = null) { |
|
validateSchema = validateSchema || ajv.compile(configSchema); |
|
|
|
if (!validateSchema(config)) { |
|
throw new Error(`ESLint configuration in ${source} is invalid:\n${this.formatErrors(validateSchema.errors)}`); |
|
} |
|
|
|
if (Object.hasOwnProperty.call(config, "ecmaFeatures")) { |
|
emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES"); |
|
} |
|
} |
|
|
|
/** |
|
* Validates an entire config object. |
|
* @param {Object} config The config object to validate. |
|
* @param {string} source The name of the configuration source to report in any errors. |
|
* @param {function(ruleId:string): Object} [getAdditionalRule] A map from strings to loaded rules. |
|
* @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded envs. |
|
* @returns {void} |
|
*/ |
|
validate(config, source, getAdditionalRule, getAdditionalEnv) { |
|
this.validateConfigSchema(config, source); |
|
this.validateRules(config.rules, source, getAdditionalRule); |
|
this.validateEnvironment(config.env, source, getAdditionalEnv); |
|
this.validateGlobals(config.globals, source); |
|
|
|
for (const override of config.overrides || []) { |
|
this.validateRules(override.rules, source, getAdditionalRule); |
|
this.validateEnvironment(override.env, source, getAdditionalEnv); |
|
this.validateGlobals(config.globals, source); |
|
} |
|
} |
|
|
|
/** |
|
* Validate config array object. |
|
* @param {ConfigArray} configArray The config array to validate. |
|
* @returns {void} |
|
*/ |
|
validateConfigArray(configArray) { |
|
const getPluginEnv = Map.prototype.get.bind(configArray.pluginEnvironments); |
|
const getPluginProcessor = Map.prototype.get.bind(configArray.pluginProcessors); |
|
const getPluginRule = Map.prototype.get.bind(configArray.pluginRules); |
|
|
|
// Validate. |
|
for (const element of configArray) { |
|
if (validated.has(element)) { |
|
continue; |
|
} |
|
validated.add(element); |
|
|
|
this.validateEnvironment(element.env, element.name, getPluginEnv); |
|
this.validateGlobals(element.globals, element.name); |
|
this.validateProcessor(element.processor, element.name, getPluginProcessor); |
|
this.validateRules(element.rules, element.name, getPluginRule); |
|
} |
|
} |
|
|
|
}
|
|
|