UNPKG

9.67 kBJavaScriptView Raw
1/**
2 * @fileoverview Validates configs.
3 * @author Brandon Mills
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const path = require("path"),
13 ajv = require("../util/ajv"),
14 lodash = require("lodash"),
15 configSchema = require("../../conf/config-schema.js"),
16 util = require("util");
17
18const ruleValidators = new WeakMap();
19
20//------------------------------------------------------------------------------
21// Private
22//------------------------------------------------------------------------------
23let validateSchema;
24
25// Defitions for deprecation warnings.
26const deprecationWarningMessages = {
27 ESLINT_LEGACY_ECMAFEATURES: "The 'ecmaFeatures' config file property is deprecated, and has no effect.",
28 ESLINT_LEGACY_OBJECT_REST_SPREAD: "The 'parserOptions.ecmaFeatures.experimentalObjectRestSpread' option is deprecated. Use 'parserOptions.ecmaVersion' instead."
29};
30const severityMap = {
31 error: 2,
32 warn: 1,
33 off: 0
34};
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 */
41function getRuleOptionsSchema(rule) {
42 const schema = rule.schema || rule.meta && rule.meta.schema;
43
44 // Given a tuple of schemas, insert warning level at the beginning
45 if (Array.isArray(schema)) {
46 if (schema.length) {
47 return {
48 type: "array",
49 items: schema,
50 minItems: 0,
51 maxItems: schema.length
52 };
53 }
54 return {
55 type: "array",
56 minItems: 0,
57 maxItems: 0
58 };
59
60 }
61
62 // Given a full schema, leave it alone
63 return schema || null;
64}
65
66/**
67 * Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
68 * @param {options} options The given options for the rule.
69 * @returns {number|string} The rule's severity value
70 */
71function validateRuleSeverity(options) {
72 const severity = Array.isArray(options) ? options[0] : options;
73 const normSeverity = typeof severity === "string" ? severityMap[severity.toLowerCase()] : severity;
74
75 if (normSeverity === 0 || normSeverity === 1 || normSeverity === 2) {
76 return normSeverity;
77 }
78
79 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`);
80
81}
82
83/**
84 * Validates the non-severity options passed to a rule, based on its schema.
85 * @param {{create: Function}} rule The rule to validate
86 * @param {Array} localOptions The options for the rule, excluding severity
87 * @returns {void}
88 */
89function validateRuleSchema(rule, localOptions) {
90 if (!ruleValidators.has(rule)) {
91 const schema = getRuleOptionsSchema(rule);
92
93 if (schema) {
94 ruleValidators.set(rule, ajv.compile(schema));
95 }
96 }
97
98 const validateRule = ruleValidators.get(rule);
99
100 if (validateRule) {
101 validateRule(localOptions);
102 if (validateRule.errors) {
103 throw new Error(validateRule.errors.map(
104 error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
105 ).join(""));
106 }
107 }
108}
109
110/**
111 * Validates a rule's options against its schema.
112 * @param {{create: Function}|null} rule The rule that the config is being validated for
113 * @param {string} ruleId The rule's unique name.
114 * @param {Array|number} options The given options for the rule.
115 * @param {string|null} source The name of the configuration source to report in any errors. If null or undefined,
116 * no source is prepended to the message.
117 * @returns {void}
118 */
119function validateRuleOptions(rule, ruleId, options, source = null) {
120 if (!rule) {
121 return;
122 }
123 try {
124 const severity = validateRuleSeverity(options);
125
126 if (severity !== 0) {
127 validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
128 }
129 } catch (err) {
130 const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`;
131
132 if (typeof source === "string") {
133 throw new Error(`${source}:\n\t${enhancedMessage}`);
134 } else {
135 throw new Error(enhancedMessage);
136 }
137 }
138}
139
140/**
141 * Validates an environment object
142 * @param {Object} environment The environment config object to validate.
143 * @param {Environments} envContext Env context
144 * @param {string} source The name of the configuration source to report in any errors.
145 * @returns {void}
146 */
147function validateEnvironment(environment, envContext, source = null) {
148
149 // not having an environment is ok
150 if (!environment) {
151 return;
152 }
153
154 Object.keys(environment).forEach(env => {
155 if (!envContext.get(env)) {
156 const message = `${source}:\n\tEnvironment key "${env}" is unknown\n`;
157
158 throw new Error(message);
159 }
160 });
161}
162
163/**
164 * Validates a rules config object
165 * @param {Object} rulesConfig The rules config object to validate.
166 * @param {function(string): {create: Function}} ruleMapper A mapper function from strings to loaded rules
167 * @param {string} source The name of the configuration source to report in any errors.
168 * @returns {void}
169 */
170function validateRules(rulesConfig, ruleMapper, source = null) {
171 if (!rulesConfig) {
172 return;
173 }
174
175 Object.keys(rulesConfig).forEach(id => {
176 validateRuleOptions(ruleMapper(id), id, rulesConfig[id], source);
177 });
178}
179
180/**
181 * Formats an array of schema validation errors.
182 * @param {Array} errors An array of error messages to format.
183 * @returns {string} Formatted error message
184 */
185function formatErrors(errors) {
186 return errors.map(error => {
187 if (error.keyword === "additionalProperties") {
188 const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
189
190 return `Unexpected top-level property "${formattedPropertyPath}"`;
191 }
192 if (error.keyword === "type") {
193 const formattedField = error.dataPath.slice(1);
194 const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
195 const formattedValue = JSON.stringify(error.data);
196
197 return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
198 }
199
200 const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
201
202 return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
203 }).map(message => `\t- ${message}.\n`).join("");
204}
205
206/**
207 * Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
208 * for each unique file path, but repeated invocations with the same file path have no effect.
209 * No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active.
210 * @param {string} source The name of the configuration source to report the warning for.
211 * @param {string} errorCode The warning message to show.
212 * @returns {void}
213 */
214const emitDeprecationWarning = lodash.memoize((source, errorCode) => {
215 const rel = path.relative(process.cwd(), source);
216 const message = deprecationWarningMessages[errorCode];
217
218 process.emitWarning(
219 `${message} (found in "${rel}")`,
220 "DeprecationWarning",
221 errorCode
222 );
223});
224
225/**
226 * Validates the top level properties of the config object.
227 * @param {Object} config The config object to validate.
228 * @param {string} source The name of the configuration source to report in any errors.
229 * @returns {void}
230 */
231function validateConfigSchema(config, source = null) {
232 validateSchema = validateSchema || ajv.compile(configSchema);
233
234 if (!validateSchema(config)) {
235 throw new Error(`ESLint configuration in ${source} is invalid:\n${formatErrors(validateSchema.errors)}`);
236 }
237
238 if (Object.hasOwnProperty.call(config, "ecmaFeatures")) {
239 emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES");
240 }
241
242 if (
243 (config.parser || "espree") === "espree" &&
244 config.parserOptions &&
245 config.parserOptions.ecmaFeatures &&
246 config.parserOptions.ecmaFeatures.experimentalObjectRestSpread
247 ) {
248 emitDeprecationWarning(source, "ESLINT_LEGACY_OBJECT_REST_SPREAD");
249 }
250}
251
252/**
253 * Validates an entire config object.
254 * @param {Object} config The config object to validate.
255 * @param {function(string): {create: Function}} ruleMapper A mapper function from rule IDs to defined rules
256 * @param {Environments} envContext The env context
257 * @param {string} source The name of the configuration source to report in any errors.
258 * @returns {void}
259 */
260function validate(config, ruleMapper, envContext, source = null) {
261 validateConfigSchema(config, source);
262 validateRules(config.rules, ruleMapper, source);
263 validateEnvironment(config.env, envContext, source);
264
265 for (const override of config.overrides || []) {
266 validateRules(override.rules, ruleMapper, source);
267 validateEnvironment(override.env, envContext, source);
268 }
269}
270
271//------------------------------------------------------------------------------
272// Public Interface
273//------------------------------------------------------------------------------
274
275module.exports = {
276 getRuleOptionsSchema,
277 validate,
278 validateRuleOptions
279};