1 | /**
|
2 | * @fileoverview Responsible for loading config files
|
3 | * @author Seth McLaughlin
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const 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 |
|
20 | const debug = require("debug")("eslint:config");
|
21 |
|
22 | //------------------------------------------------------------------------------
|
23 | // Constants
|
24 | //------------------------------------------------------------------------------
|
25 |
|
26 | const PERSONAL_CONFIG_DIR = os.homedir();
|
27 | const 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 | */
|
39 | function 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 | */
|
48 | function 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 | */
|
64 | class 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 |
|
377 | module.exports = Config;
|