UNPKG

15.6 kBJavaScriptView Raw
1/**
2 * @fileoverview Config file operations. This file must be usable in the browser,
3 * so no Node-specific code can be here.
4 * @author Nicholas C. Zakas
5 */
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const minimatch = require("minimatch"),
13 path = require("path");
14
15const debug = require("debug")("eslint:config-ops");
16
17//------------------------------------------------------------------------------
18// Private
19//------------------------------------------------------------------------------
20
21const RULE_SEVERITY_STRINGS = ["off", "warn", "error"],
22 RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => {
23 map[value] = index;
24 return map;
25 }, {}),
26 VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"];
27
28//------------------------------------------------------------------------------
29// Public Interface
30//------------------------------------------------------------------------------
31
32module.exports = {
33
34 /**
35 * Creates an empty configuration object suitable for merging as a base.
36 * @returns {Object} A configuration object.
37 */
38 createEmptyConfig() {
39 return {
40 globals: {},
41 env: {},
42 rules: {},
43 parserOptions: {}
44 };
45 },
46
47 /**
48 * Creates an environment config based on the specified environments.
49 * @param {Object<string,boolean>} env The environment settings.
50 * @param {Environments} envContext The environment context.
51 * @returns {Object} A configuration object with the appropriate rules and globals
52 * set.
53 */
54 createEnvironmentConfig(env, envContext) {
55
56 const envConfig = this.createEmptyConfig();
57
58 if (env) {
59
60 envConfig.env = env;
61
62 Object.keys(env).filter(name => env[name]).forEach(name => {
63 const environment = envContext.get(name);
64
65 if (environment) {
66 debug(`Creating config for environment ${name}`);
67 if (environment.globals) {
68 Object.assign(envConfig.globals, environment.globals);
69 }
70
71 if (environment.parserOptions) {
72 Object.assign(envConfig.parserOptions, environment.parserOptions);
73 }
74 }
75 });
76 }
77
78 return envConfig;
79 },
80
81 /**
82 * Given a config with environment settings, applies the globals and
83 * ecmaFeatures to the configuration and returns the result.
84 * @param {Object} config The configuration information.
85 * @param {Environments} envContent env context.
86 * @returns {Object} The updated configuration information.
87 */
88 applyEnvironments(config, envContent) {
89 if (config.env && typeof config.env === "object") {
90 debug("Apply environment settings to config");
91 return this.merge(this.createEnvironmentConfig(config.env, envContent), config);
92 }
93
94 return config;
95 },
96
97 /**
98 * Merges two config objects. This will not only add missing keys, but will also modify values to match.
99 * @param {Object} target config object
100 * @param {Object} src config object. Overrides in this config object will take priority over base.
101 * @param {boolean} [combine] Whether to combine arrays or not
102 * @param {boolean} [isRule] Whether its a rule
103 * @returns {Object} merged config object.
104 */
105 merge: function deepmerge(target, src, combine, isRule) {
106
107 /*
108 * The MIT License (MIT)
109 *
110 * Copyright (c) 2012 Nicholas Fisher
111 *
112 * Permission is hereby granted, free of charge, to any person obtaining a copy
113 * of this software and associated documentation files (the "Software"), to deal
114 * in the Software without restriction, including without limitation the rights
115 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
116 * copies of the Software, and to permit persons to whom the Software is
117 * furnished to do so, subject to the following conditions:
118 *
119 * The above copyright notice and this permission notice shall be included in
120 * all copies or substantial portions of the Software.
121 *
122 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
123 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
124 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
125 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
126 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
127 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
128 * THE SOFTWARE.
129 */
130
131 /*
132 * This code is taken from deepmerge repo
133 * (https://github.com/KyleAMathews/deepmerge)
134 * and modified to meet our needs.
135 */
136 const array = Array.isArray(src) || Array.isArray(target);
137 let dst = array && [] || {};
138
139 if (array) {
140 const resolvedTarget = target || [];
141
142 // src could be a string, so check for array
143 if (isRule && Array.isArray(src) && src.length > 1) {
144 dst = dst.concat(src);
145 } else {
146 dst = dst.concat(resolvedTarget);
147 }
148 const resolvedSrc = typeof src === "object" ? src : [src];
149
150 Object.keys(resolvedSrc).forEach((_, i) => {
151 const e = resolvedSrc[i];
152
153 if (typeof dst[i] === "undefined") {
154 dst[i] = e;
155 } else if (typeof e === "object") {
156 if (isRule) {
157 dst[i] = e;
158 } else {
159 dst[i] = deepmerge(resolvedTarget[i], e, combine, isRule);
160 }
161 } else {
162 if (!combine) {
163 dst[i] = e;
164 } else {
165 if (dst.indexOf(e) === -1) {
166 dst.push(e);
167 }
168 }
169 }
170 });
171 } else {
172 if (target && typeof target === "object") {
173 Object.keys(target).forEach(key => {
174 dst[key] = target[key];
175 });
176 }
177 Object.keys(src).forEach(key => {
178 if (key === "overrides") {
179 dst[key] = (target[key] || []).concat(src[key] || []);
180 } else if (Array.isArray(src[key]) || Array.isArray(target[key])) {
181 dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule);
182 } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
183 dst[key] = src[key];
184 } else {
185 dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules");
186 }
187 });
188 }
189
190 return dst;
191 },
192
193 /**
194 * Normalizes the severity value of a rule's configuration to a number
195 * @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally
196 * received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0),
197 * the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array
198 * whose first element is one of the above values. Strings are matched case-insensitively.
199 * @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0.
200 */
201 getRuleSeverity(ruleConfig) {
202 const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
203
204 if (severityValue === 0 || severityValue === 1 || severityValue === 2) {
205 return severityValue;
206 }
207
208 if (typeof severityValue === "string") {
209 return RULE_SEVERITY[severityValue.toLowerCase()] || 0;
210 }
211
212 return 0;
213 },
214
215 /**
216 * Converts old-style severity settings (0, 1, 2) into new-style
217 * severity settings (off, warn, error) for all rules. Assumption is that severity
218 * values have already been validated as correct.
219 * @param {Object} config The config object to normalize.
220 * @returns {void}
221 */
222 normalizeToStrings(config) {
223
224 if (config.rules) {
225 Object.keys(config.rules).forEach(ruleId => {
226 const ruleConfig = config.rules[ruleId];
227
228 if (typeof ruleConfig === "number") {
229 config.rules[ruleId] = RULE_SEVERITY_STRINGS[ruleConfig] || RULE_SEVERITY_STRINGS[0];
230 } else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "number") {
231 ruleConfig[0] = RULE_SEVERITY_STRINGS[ruleConfig[0]] || RULE_SEVERITY_STRINGS[0];
232 }
233 });
234 }
235 },
236
237 /**
238 * Determines if the severity for the given rule configuration represents an error.
239 * @param {int|string|Array} ruleConfig The configuration for an individual rule.
240 * @returns {boolean} True if the rule represents an error, false if not.
241 */
242 isErrorSeverity(ruleConfig) {
243 return module.exports.getRuleSeverity(ruleConfig) === 2;
244 },
245
246 /**
247 * Checks whether a given config has valid severity or not.
248 * @param {number|string|Array} ruleConfig - The configuration for an individual rule.
249 * @returns {boolean} `true` if the configuration has valid severity.
250 */
251 isValidSeverity(ruleConfig) {
252 let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
253
254 if (typeof severity === "string") {
255 severity = severity.toLowerCase();
256 }
257 return VALID_SEVERITIES.indexOf(severity) !== -1;
258 },
259
260 /**
261 * Checks whether every rule of a given config has valid severity or not.
262 * @param {Object} config - The configuration for rules.
263 * @returns {boolean} `true` if the configuration has valid severity.
264 */
265 isEverySeverityValid(config) {
266 return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId]));
267 },
268
269 /**
270 * Merges all configurations in a given config vector. A vector is an array of objects, each containing a config
271 * file path and a list of subconfig indices that match the current file path. All config data is assumed to be
272 * cached.
273 * @param {Array<Object>} vector list of config files and their subconfig indices that match the current file path
274 * @param {Object} configCache the config cache
275 * @returns {Object} config object
276 */
277 getConfigFromVector(vector, configCache) {
278
279 const cachedConfig = configCache.getMergedVectorConfig(vector);
280
281 if (cachedConfig) {
282 return cachedConfig;
283 }
284
285 debug("Using config from partial cache");
286
287 const subvector = Array.from(vector);
288 let nearestCacheIndex = subvector.length - 1,
289 partialCachedConfig;
290
291 while (nearestCacheIndex >= 0) {
292 partialCachedConfig = configCache.getMergedVectorConfig(subvector);
293 if (partialCachedConfig) {
294 break;
295 }
296 subvector.pop();
297 nearestCacheIndex--;
298 }
299
300 if (!partialCachedConfig) {
301 partialCachedConfig = {};
302 }
303
304 let finalConfig = partialCachedConfig;
305
306 // Start from entry immediately following nearest cached config (first uncached entry)
307 for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
308 finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache);
309 configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig);
310 }
311
312 return finalConfig;
313 },
314
315 /**
316 * Merges the config options from a single vector entry into the supplied config.
317 * @param {Object} config the base config to merge the vector entry's options into
318 * @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of
319 * matching override indices
320 * @param {Object} configCache the config cache
321 * @returns {Object} merged config object
322 */
323 mergeVectorEntry(config, vectorEntry, configCache) {
324 const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath));
325 let mergedConfig = Object.assign({}, config),
326 overrides;
327
328 if (vectorEntryConfig.overrides) {
329 overrides = vectorEntryConfig.overrides.filter(
330 (override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1
331 );
332 } else {
333 overrides = [];
334 }
335
336 mergedConfig = this.merge(mergedConfig, vectorEntryConfig);
337
338 delete mergedConfig.overrides;
339
340 mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig);
341
342 if (mergedConfig.filePath) {
343 delete mergedConfig.filePath;
344 delete mergedConfig.baseDirectory;
345 } else if (mergedConfig.files) {
346 delete mergedConfig.files;
347 }
348
349 return mergedConfig;
350 },
351
352 /**
353 * Checks that the specified file path matches all of the supplied glob patterns.
354 * @param {string} filePath The file path to test patterns against
355 * @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path
356 * @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path
357 * @returns {boolean} True if all the supplied patterns match the file path, false otherwise
358 */
359 pathMatchesGlobs(filePath, patterns, excludedPatterns) {
360 const patternList = [].concat(patterns);
361 const excludedPatternList = [].concat(excludedPatterns || []);
362
363 patternList.concat(excludedPatternList).forEach(pattern => {
364 if (path.isAbsolute(pattern) || pattern.includes("..")) {
365 throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
366 }
367 });
368
369 const opts = { matchBase: true };
370
371 return patternList.some(pattern => minimatch(filePath, pattern, opts)) &&
372 !excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts));
373 },
374
375 /**
376 * Normalizes a value for a global in a config
377 * @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in
378 * a global directive comment
379 * @returns {("readable"|"writeable"|"off")} The value normalized as a string
380 */
381 normalizeConfigGlobal(configuredValue) {
382 switch (configuredValue) {
383 case "off":
384 return "off";
385
386 case true:
387 case "true":
388 case "writeable":
389 case "writable":
390 return "writeable";
391
392 case null:
393 case false:
394 case "false":
395 case "readable":
396 case "readonly":
397 return "readable";
398
399 // Fallback to minimize compatibility impact
400 default:
401 return "writeable";
402 }
403 }
404};