2020-03-07 11:45:40 +08:00
/ * *
* @ fileoverview Validates configs .
* @ author Brandon Mills
* /
"use strict" ;
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
2020-03-31 20:40:00 +08:00
const
path = require ( "path" ) ,
util = require ( "util" ) ,
2020-03-07 11:45:40 +08:00
lodash = require ( "lodash" ) ,
2020-03-31 20:40:00 +08:00
configSchema = require ( "../../conf/config-schema" ) ,
BuiltInEnvironments = require ( "../../conf/environments" ) ,
BuiltInRules = require ( "../rules" ) ,
ConfigOps = require ( "./config-ops" ) ;
2020-03-07 11:45:40 +08:00
2020-03-31 20:40:00 +08:00
const ajv = require ( "./ajv" ) ( ) ;
2020-03-07 11:45:40 +08:00
const ruleValidators = new WeakMap ( ) ;
2020-03-31 20:40:00 +08:00
const noop = Function . prototype ;
2020-03-07 11:45:40 +08:00
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
let validateSchema ;
// Defitions for deprecation warnings.
const deprecationWarningMessages = {
2020-03-31 20:40:00 +08:00
ESLINT _LEGACY _ECMAFEATURES : "The 'ecmaFeatures' config file property is deprecated, and has no effect."
2020-03-07 11:45:40 +08:00
} ;
const severityMap = {
error : 2 ,
warn : 1 ,
off : 0
} ;
/ * *
* 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 .
* /
function getRuleOptionsSchema ( rule ) {
2020-03-31 20:40:00 +08:00
if ( ! rule ) {
return null ;
}
2020-03-07 11:45:40 +08:00
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
* /
function 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 ( ` \t Severity 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 }
* /
function validateRuleSchema ( rule , localOptions ) {
if ( ! ruleValidators . has ( rule ) ) {
const schema = 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 => ` \t Value ${ 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 }
* /
function validateRuleOptions ( rule , ruleId , options , source = null ) {
try {
const severity = validateRuleSeverity ( options ) ;
if ( severity !== 0 ) {
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 .
2020-03-31 20:40:00 +08:00
* @ param { function ( envId : string ) : Object } [ getAdditionalEnv ] A map from strings to loaded environments .
2020-03-07 11:45:40 +08:00
* @ returns { void }
* /
2020-03-31 20:40:00 +08:00
function validateEnvironment (
environment ,
source ,
getAdditionalEnv = noop
) {
2020-03-07 11:45:40 +08:00
// not having an environment is ok
if ( ! environment ) {
return ;
}
2020-03-31 20:40:00 +08:00
Object . keys ( environment ) . forEach ( id => {
const env = getAdditionalEnv ( id ) || BuiltInEnvironments . get ( id ) || null ;
if ( ! env ) {
const message = ` ${ source } : \n \t Environment key " ${ id } " is unknown \n ` ;
2020-03-07 11:45:40 +08:00
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 .
2020-03-31 20:40:00 +08:00
* @ param { function ( ruleId : string ) : Object } getAdditionalRule A map from strings to loaded rules
2020-03-07 11:45:40 +08:00
* @ returns { void }
* /
2020-03-31 20:40:00 +08:00
function validateRules (
rulesConfig ,
source ,
getAdditionalRule = noop
) {
2020-03-07 11:45:40 +08:00
if ( ! rulesConfig ) {
return ;
}
Object . keys ( rulesConfig ) . forEach ( id => {
2020-03-31 20:40:00 +08:00
const rule = getAdditionalRule ( id ) || BuiltInRules . get ( id ) || null ;
validateRuleOptions ( rule , id , rulesConfig [ id ] , source ) ;
2020-03-07 11:45:40 +08:00
} ) ;
}
2020-03-31 20:40:00 +08:00
/ * *
* Validates a ` globals ` section of a config file
* @ param { Object } globalsConfig The ` glboals ` section
* @ param { string | null } source The name of the configuration source to report in the event of an error .
* @ returns { void }
* /
function 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 }
* /
function validateProcessor ( processorName , source , getProcessor ) {
if ( processorName && ! getProcessor ( processorName ) ) {
throw new Error ( ` ESLint configuration of processor in ' ${ source } ' is invalid: ' ${ processorName } ' was not found. ` ) ;
}
}
2020-03-07 11:45:40 +08:00
/ * *
* Formats an array of schema validation errors .
* @ param { Array } errors An array of error messages to format .
* @ returns { string } Formatted error message
* /
function 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 ( "" ) ;
}
/ * *
* Emits a deprecation warning containing a given filepath . A new deprecation warning is emitted
* for each unique file path , but repeated invocations with the same file path have no effect .
* No warnings are emitted if the ` --no-deprecation ` or ` --no-warnings ` Node runtime flags are active .
* @ param { string } source The name of the configuration source to report the warning for .
* @ param { string } errorCode The warning message to show .
* @ returns { void }
* /
const emitDeprecationWarning = lodash . memoize ( ( source , errorCode ) => {
const rel = path . relative ( process . cwd ( ) , source ) ;
const message = deprecationWarningMessages [ errorCode ] ;
process . emitWarning (
` ${ message } (found in " ${ rel } ") ` ,
"DeprecationWarning" ,
errorCode
) ;
} ) ;
/ * *
* 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 }
* /
function validateConfigSchema ( config , source = null ) {
validateSchema = validateSchema || ajv . compile ( configSchema ) ;
if ( ! validateSchema ( config ) ) {
throw new Error ( ` ESLint configuration in ${ source } is invalid: \n ${ 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 .
2020-03-31 20:40:00 +08:00
* @ 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 .
2020-03-07 11:45:40 +08:00
* @ returns { void }
* /
2020-03-31 20:40:00 +08:00
function validate ( config , source , getAdditionalRule , getAdditionalEnv ) {
2020-03-07 11:45:40 +08:00
validateConfigSchema ( config , source ) ;
2020-03-31 20:40:00 +08:00
validateRules ( config . rules , source , getAdditionalRule ) ;
validateEnvironment ( config . env , source , getAdditionalEnv ) ;
validateGlobals ( config . globals , source ) ;
2020-03-07 11:45:40 +08:00
for ( const override of config . overrides || [ ] ) {
2020-03-31 20:40:00 +08:00
validateRules ( override . rules , source , getAdditionalRule ) ;
validateEnvironment ( override . env , source , getAdditionalEnv ) ;
validateGlobals ( config . globals , source ) ;
}
}
const validated = new WeakSet ( ) ;
/ * *
* Validate config array object .
* @ param { ConfigArray } configArray The config array to validate .
* @ returns { void }
* /
function 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 ) ;
validateEnvironment ( element . env , element . name , getPluginEnv ) ;
validateGlobals ( element . globals , element . name ) ;
validateProcessor ( element . processor , element . name , getPluginProcessor ) ;
validateRules ( element . rules , element . name , getPluginRule ) ;
2020-03-07 11:45:40 +08:00
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module . exports = {
getRuleOptionsSchema ,
validate ,
2020-03-31 20:40:00 +08:00
validateConfigArray ,
validateConfigSchema ,
2020-03-07 11:45:40 +08:00
validateRuleOptions
} ;