UNPKG

14.8 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 combine = !!combine;
140 isRule = !!isRule;
141 if (array) {
142 target = target || [];
143
144 // src could be a string, so check for array
145 if (isRule && Array.isArray(src) && src.length > 1) {
146 dst = dst.concat(src);
147 } else {
148 dst = dst.concat(target);
149 }
150 if (typeof src !== "object" && !Array.isArray(src)) {
151 src = [src];
152 }
153 Object.keys(src).forEach((e, i) => {
154 e = src[i];
155 if (typeof dst[i] === "undefined") {
156 dst[i] = e;
157 } else if (typeof e === "object") {
158 if (isRule) {
159 dst[i] = e;
160 } else {
161 dst[i] = deepmerge(target[i], e, combine, isRule);
162 }
163 } else {
164 if (!combine) {
165 dst[i] = e;
166 } else {
167 if (dst.indexOf(e) === -1) {
168 dst.push(e);
169 }
170 }
171 }
172 });
173 } else {
174 if (target && typeof target === "object") {
175 Object.keys(target).forEach(key => {
176 dst[key] = target[key];
177 });
178 }
179 Object.keys(src).forEach(key => {
180 if (key === "overrides") {
181 dst[key] = (target[key] || []).concat(src[key] || []);
182 } else if (Array.isArray(src[key]) || Array.isArray(target[key])) {
183 dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule);
184 } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
185 dst[key] = src[key];
186 } else {
187 dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules");
188 }
189 });
190 }
191
192 return dst;
193 },
194
195 /**
196 * Converts new-style severity settings (off, warn, error) into old-style
197 * severity settings (0, 1, 2) for all rules. Assumption is that severity
198 * values have already been validated as correct.
199 * @param {Object} config The config object to normalize.
200 * @returns {void}
201 */
202 normalize(config) {
203
204 if (config.rules) {
205 Object.keys(config.rules).forEach(ruleId => {
206 const ruleConfig = config.rules[ruleId];
207
208 if (typeof ruleConfig === "string") {
209 config.rules[ruleId] = RULE_SEVERITY[ruleConfig.toLowerCase()] || 0;
210 } else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "string") {
211 ruleConfig[0] = RULE_SEVERITY[ruleConfig[0].toLowerCase()] || 0;
212 }
213 });
214 }
215 },
216
217 /**
218 * Converts old-style severity settings (0, 1, 2) into new-style
219 * severity settings (off, warn, error) for all rules. Assumption is that severity
220 * values have already been validated as correct.
221 * @param {Object} config The config object to normalize.
222 * @returns {void}
223 */
224 normalizeToStrings(config) {
225
226 if (config.rules) {
227 Object.keys(config.rules).forEach(ruleId => {
228 const ruleConfig = config.rules[ruleId];
229
230 if (typeof ruleConfig === "number") {
231 config.rules[ruleId] = RULE_SEVERITY_STRINGS[ruleConfig] || RULE_SEVERITY_STRINGS[0];
232 } else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "number") {
233 ruleConfig[0] = RULE_SEVERITY_STRINGS[ruleConfig[0]] || RULE_SEVERITY_STRINGS[0];
234 }
235 });
236 }
237 },
238
239 /**
240 * Determines if the severity for the given rule configuration represents an error.
241 * @param {int|string|Array} ruleConfig The configuration for an individual rule.
242 * @returns {boolean} True if the rule represents an error, false if not.
243 */
244 isErrorSeverity(ruleConfig) {
245
246 let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
247
248 if (typeof severity === "string") {
249 severity = RULE_SEVERITY[severity.toLowerCase()] || 0;
250 }
251
252 return (typeof severity === "number" && severity === 2);
253 },
254
255 /**
256 * Checks whether a given config has valid severity or not.
257 * @param {number|string|Array} ruleConfig - The configuration for an individual rule.
258 * @returns {boolean} `true` if the configuration has valid severity.
259 */
260 isValidSeverity(ruleConfig) {
261 let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
262
263 if (typeof severity === "string") {
264 severity = severity.toLowerCase();
265 }
266 return VALID_SEVERITIES.indexOf(severity) !== -1;
267 },
268
269 /**
270 * Checks whether every rule of a given config has valid severity or not.
271 * @param {Object} config - The configuration for rules.
272 * @returns {boolean} `true` if the configuration has valid severity.
273 */
274 isEverySeverityValid(config) {
275 return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId]));
276 },
277
278 /**
279 * Merges all configurations in a given config vector. A vector is an array of objects, each containing a config
280 * file path and a list of subconfig indices that match the current file path. All config data is assumed to be
281 * cached.
282 * @param {Array<Object>} vector list of config files and their subconfig indices that match the current file path
283 * @param {Object} configCache the config cache
284 * @returns {Object} config object
285 */
286 getConfigFromVector(vector, configCache) {
287
288 const cachedConfig = configCache.getMergedVectorConfig(vector);
289
290 if (cachedConfig) {
291 return cachedConfig;
292 }
293
294 debug("Using config from partial cache");
295
296 const subvector = Array.from(vector);
297 let nearestCacheIndex = subvector.length - 1,
298 partialCachedConfig;
299
300 while (nearestCacheIndex >= 0) {
301 partialCachedConfig = configCache.getMergedVectorConfig(subvector);
302 if (partialCachedConfig) {
303 break;
304 }
305 subvector.pop();
306 nearestCacheIndex--;
307 }
308
309 if (!partialCachedConfig) {
310 partialCachedConfig = {};
311 }
312
313 let finalConfig = partialCachedConfig;
314
315 // Start from entry immediately following nearest cached config (first uncached entry)
316 for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
317 finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache);
318 configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig);
319 }
320
321 return finalConfig;
322 },
323
324 /**
325 * Merges the config options from a single vector entry into the supplied config.
326 * @param {Object} config the base config to merge the vector entry's options into
327 * @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of
328 * matching override indices
329 * @param {Object} configCache the config cache
330 * @returns {Object} merged config object
331 */
332 mergeVectorEntry(config, vectorEntry, configCache) {
333 const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath));
334 let mergedConfig = Object.assign({}, config),
335 overrides;
336
337 if (vectorEntryConfig.overrides) {
338 overrides = vectorEntryConfig.overrides.filter(
339 (override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1
340 );
341 } else {
342 overrides = [];
343 }
344
345 mergedConfig = this.merge(mergedConfig, vectorEntryConfig);
346
347 delete mergedConfig.overrides;
348
349 mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig);
350
351 if (mergedConfig.filePath) {
352 delete mergedConfig.filePath;
353 delete mergedConfig.baseDirectory;
354 } else if (mergedConfig.files) {
355 delete mergedConfig.files;
356 }
357
358 return mergedConfig;
359 },
360
361 /**
362 * Checks that the specified file path matches all of the supplied glob patterns.
363 * @param {string} filePath The file path to test patterns against
364 * @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path
365 * @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path
366 * @returns {boolean} True if all the supplied patterns match the file path, false otherwise
367 */
368 pathMatchesGlobs(filePath, patterns, excludedPatterns) {
369 const patternList = [].concat(patterns);
370 const excludedPatternList = [].concat(excludedPatterns || []);
371
372 patternList.concat(excludedPatternList).forEach(pattern => {
373 if (path.isAbsolute(pattern) || pattern.includes("..")) {
374 throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
375 }
376 });
377
378 const opts = { matchBase: true };
379
380 return patternList.some(pattern => minimatch(filePath, pattern, opts)) &&
381 !excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts));
382 }
383};