UNPKG

13.9 kBJavaScriptView Raw
1/**
2 * @fileoverview Responsible for loading config files
3 * @author Seth McLaughlin
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const path = require("path"),
13 os = require("os"),
14 ConfigOps = require("./config/config-ops"),
15 ConfigFile = require("./config/config-file"),
16 ConfigCache = require("./config/config-cache"),
17 Plugins = require("./config/plugins"),
18 FileFinder = require("./util/file-finder");
19
20const debug = require("debug")("eslint:config");
21
22//------------------------------------------------------------------------------
23// Constants
24//------------------------------------------------------------------------------
25
26const PERSONAL_CONFIG_DIR = os.homedir();
27const SUBCONFIG_SEP = ":";
28
29//------------------------------------------------------------------------------
30// Helpers
31//------------------------------------------------------------------------------
32
33/**
34 * Determines if any rules were explicitly passed in as options.
35 * @param {Object} options The options used to create our configuration.
36 * @returns {boolean} True if rules were passed in as options, false otherwise.
37 * @private
38 */
39function hasRules(options) {
40 return options.rules && Object.keys(options.rules).length > 0;
41}
42
43/**
44 * Determines if a module is can be resolved.
45 * @param {string} moduleId The ID (name) of the module
46 * @returns {boolean} True if it is resolvable; False otherwise.
47 */
48function isResolvable(moduleId) {
49 try {
50 require.resolve(moduleId);
51 return true;
52 } catch (err) {
53 return false;
54 }
55}
56
57//------------------------------------------------------------------------------
58// API
59//------------------------------------------------------------------------------
60
61/**
62 * Configuration class
63 */
64class Config {
65
66 /**
67 * @param {Object} providedOptions Options to be passed in
68 * @param {Linter} linterContext Linter instance object
69 */
70 constructor(providedOptions, linterContext) {
71 const options = providedOptions || {};
72
73 this.linterContext = linterContext;
74 this.plugins = new Plugins(linterContext.environments, linterContext.defineRule.bind(linterContext));
75
76 this.options = options;
77 this.ignore = options.ignore;
78 this.ignorePath = options.ignorePath;
79 this.parser = options.parser;
80 this.parserOptions = options.parserOptions || {};
81
82 this.configCache = new ConfigCache();
83
84 this.baseConfig = options.baseConfig
85 ? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this))
86 : { rules: {} };
87 this.baseConfig.filePath = "";
88 this.baseConfig.baseDirectory = this.options.cwd;
89
90 this.configCache.setConfig(this.baseConfig.filePath, this.baseConfig);
91 this.configCache.setMergedVectorConfig(this.baseConfig.filePath, this.baseConfig);
92
93 this.useEslintrc = (options.useEslintrc !== false);
94
95 this.env = (options.envs || []).reduce((envs, name) => {
96 envs[name] = true;
97 return envs;
98 }, {});
99
100 /*
101 * Handle declared globals.
102 * For global variable foo, handle "foo:false" and "foo:true" to set
103 * whether global is writable.
104 * If user declares "foo", convert to "foo:false".
105 */
106 this.globals = (options.globals || []).reduce((globals, def) => {
107 const parts = def.split(SUBCONFIG_SEP);
108
109 globals[parts[0]] = (parts.length > 1 && parts[1] === "true");
110
111 return globals;
112 }, {});
113
114 this.loadSpecificConfig(options.configFile);
115
116 // Empty values in configs don't merge properly
117 const cliConfigOptions = {
118 env: this.env,
119 rules: this.options.rules,
120 globals: this.globals,
121 parserOptions: this.parserOptions,
122 plugins: this.options.plugins
123 };
124
125 this.cliConfig = {};
126 Object.keys(cliConfigOptions).forEach(configKey => {
127 const value = cliConfigOptions[configKey];
128
129 if (value) {
130 this.cliConfig[configKey] = value;
131 }
132 });
133 }
134
135 /**
136 * Loads the config options from a config specified on the command line.
137 * @param {string} [config] A shareable named config or path to a config file.
138 * @returns {void}
139 */
140 loadSpecificConfig(config) {
141 if (config) {
142 debug(`Using command line config ${config}`);
143 const isNamedConfig =
144 isResolvable(config) ||
145 isResolvable(`eslint-config-${config}`) ||
146 config.charAt(0) === "@";
147
148 this.specificConfig = ConfigFile.load(
149 isNamedConfig ? config : path.resolve(this.options.cwd, config),
150 this
151 );
152 }
153 }
154
155 /**
156 * Gets the personal config object from user's home directory.
157 * @returns {Object} the personal config object (null if there is no personal config)
158 * @private
159 */
160 getPersonalConfig() {
161 if (typeof this.personalConfig === "undefined") {
162 let config;
163 const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
164
165 if (filename) {
166 debug("Using personal config");
167 config = ConfigFile.load(filename, this);
168 }
169
170 this.personalConfig = config || null;
171 }
172
173 return this.personalConfig;
174 }
175
176 /**
177 * Builds a hierarchy of config objects, including the base config, all local configs from the directory tree,
178 * and a config file specified on the command line, if applicable.
179 * @param {string} directory a file in whose directory we start looking for a local config
180 * @returns {Object[]} The config objects, in ascending order of precedence
181 * @private
182 */
183 getConfigHierarchy(directory) {
184 debug(`Constructing config file hierarchy for ${directory}`);
185
186 // Step 1: Always include baseConfig
187 let configs = [this.baseConfig];
188
189 // Step 2: Add user-specified config from .eslintrc.* and package.json files
190 if (this.useEslintrc) {
191 debug("Using .eslintrc and package.json files");
192 configs = configs.concat(this.getLocalConfigHierarchy(directory));
193 } else {
194 debug("Not using .eslintrc or package.json files");
195 }
196
197 // Step 3: Merge in command line config file
198 if (this.specificConfig) {
199 debug("Using command line config file");
200 configs.push(this.specificConfig);
201 }
202
203 return configs;
204 }
205
206 /**
207 * Gets a list of config objects extracted from local config files that apply to the current directory, in
208 * descending order, beginning with the config that is highest in the directory tree.
209 * @param {string} directory The directory to start looking in for local config files.
210 * @returns {Object[]} The shallow local config objects, in ascending order of precedence (closest to the current
211 * directory at the end), or an empty array if there are no local configs.
212 * @private
213 */
214 getLocalConfigHierarchy(directory) {
215 const localConfigFiles = this.findLocalConfigFiles(directory),
216 projectConfigPath = ConfigFile.getFilenameForDirectory(this.options.cwd),
217 searched = [],
218 configs = [];
219
220 for (const localConfigFile of localConfigFiles) {
221 const localConfigDirectory = path.dirname(localConfigFile);
222 const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory);
223
224 if (localConfigHierarchyCache) {
225 const localConfigHierarchy = localConfigHierarchyCache.concat(configs);
226
227 this.configCache.setHierarchyLocalConfigs(searched, localConfigHierarchy);
228 return localConfigHierarchy;
229 }
230
231 /*
232 * Don't consider the personal config file in the home directory,
233 * except if the home directory is the same as the current working directory
234 */
235 if (localConfigDirectory === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {
236 continue;
237 }
238
239 debug(`Loading ${localConfigFile}`);
240 const localConfig = ConfigFile.load(localConfigFile, this);
241
242 // Ignore empty config files
243 if (!localConfig) {
244 continue;
245 }
246
247 debug(`Using ${localConfigFile}`);
248 configs.unshift(localConfig);
249 searched.push(localConfigDirectory);
250
251 // Stop traversing if a config is found with the root flag set
252 if (localConfig.root) {
253 break;
254 }
255 }
256
257 if (!configs.length && !this.specificConfig) {
258
259 // Fall back on the personal config from ~/.eslintrc
260 debug("Using personal config file");
261 const personalConfig = this.getPersonalConfig();
262
263 if (personalConfig) {
264 configs.unshift(personalConfig);
265 } else if (!hasRules(this.options) && !this.options.baseConfig) {
266
267 // No config file, no manual configuration, and no rules, so error.
268 const noConfigError = new Error("No ESLint configuration found.");
269
270 noConfigError.messageTemplate = "no-config-found";
271 noConfigError.messageData = {
272 directory,
273 filesExamined: localConfigFiles
274 };
275
276 throw noConfigError;
277 }
278 }
279
280 // Set the caches for the parent directories
281 this.configCache.setHierarchyLocalConfigs(searched, configs);
282
283 return configs;
284 }
285
286 /**
287 * Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of
288 * entries, each of which in an object specifying a config file path and an array of override indices corresponding
289 * to entries in the config file's overrides section whose glob patterns match the specified file path; e.g., the
290 * vector entry { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main
291 * project .eslintrc file and its first and third override blocks apply to the current file.
292 * @param {string} filePath The file path for which to build the hierarchy and config vector.
293 * @returns {Array<Object>} config vector applicable to the specified path
294 * @private
295 */
296 getConfigVector(filePath) {
297 const directory = filePath ? path.dirname(filePath) : this.options.cwd;
298
299 return this.getConfigHierarchy(directory).map(config => {
300 const vectorEntry = {
301 filePath: config.filePath,
302 matchingOverrides: []
303 };
304
305 if (config.overrides) {
306 const relativePath = path.relative(config.baseDirectory, filePath || directory);
307
308 config.overrides.forEach((override, i) => {
309 if (ConfigOps.pathMatchesGlobs(relativePath, override.files, override.excludedFiles)) {
310 vectorEntry.matchingOverrides.push(i);
311 }
312 });
313 }
314
315 return vectorEntry;
316 });
317 }
318
319 /**
320 * Finds local config files from the specified directory and its parent directories.
321 * @param {string} directory The directory to start searching from.
322 * @returns {GeneratorFunction} The paths of local config files found.
323 */
324 findLocalConfigFiles(directory) {
325 if (!this.localConfigFinder) {
326 this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);
327 }
328
329 return this.localConfigFinder.findAllInDirectoryAndParents(directory);
330 }
331
332 /**
333 * Builds the authoritative config object for the specified file path by merging the hierarchy of config objects
334 * that apply to the current file, including the base config (conf/eslint-recommended), the user's personal config
335 * from their homedir, all local configs from the directory tree, any specific config file passed on the command
336 * line, any configuration overrides set directly on the command line, and finally the environment configs
337 * (conf/environments).
338 * @param {string} filePath a file in whose directory we start looking for a local config
339 * @returns {Object} config object
340 */
341 getConfig(filePath) {
342 const vector = this.getConfigVector(filePath);
343 let config = this.configCache.getMergedConfig(vector);
344
345 if (config) {
346 debug("Using config from cache");
347 return config;
348 }
349
350 // Step 1: Merge in the filesystem configurations (base, local, and personal)
351 config = ConfigOps.getConfigFromVector(vector, this.configCache);
352
353 // Step 2: Merge in command line configurations
354 config = ConfigOps.merge(config, this.cliConfig);
355
356 if (this.cliConfig.plugins) {
357 this.plugins.loadAll(this.cliConfig.plugins);
358 }
359
360 /*
361 * Step 3: Override parser only if it is passed explicitly through the command line
362 * or if it's not defined yet (because the final object will at least have the parser key)
363 */
364 if (this.parser || !config.parser) {
365 config = ConfigOps.merge(config, { parser: this.parser });
366 }
367
368 // Step 4: Apply environments to the config
369 config = ConfigOps.applyEnvironments(config, this.linterContext.environments);
370
371 this.configCache.setMergedConfig(vector, config);
372
373 return config;
374 }
375}
376
377module.exports = Config;