diff options
Diffstat (limited to 'tools/node_modules/eslint/lib/config/config-file.js')
-rw-r--r-- | tools/node_modules/eslint/lib/config/config-file.js | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/tools/node_modules/eslint/lib/config/config-file.js b/tools/node_modules/eslint/lib/config/config-file.js new file mode 100644 index 0000000000..c5ff073cfc --- /dev/null +++ b/tools/node_modules/eslint/lib/config/config-file.js @@ -0,0 +1,595 @@ +/** + * @fileoverview Helper to locate and load configuration files. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const fs = require("fs"), + path = require("path"), + ConfigOps = require("./config-ops"), + validator = require("./config-validator"), + ModuleResolver = require("../util/module-resolver"), + naming = require("../util/naming"), + pathIsInside = require("path-is-inside"), + stripComments = require("strip-json-comments"), + stringify = require("json-stable-stringify-without-jsonify"), + requireUncached = require("require-uncached"); + +const debug = require("debug")("eslint:config-file"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Determines sort order for object keys for json-stable-stringify + * + * see: https://github.com/samn/json-stable-stringify#cmp + * + * @param {Object} a The first comparison object ({key: akey, value: avalue}) + * @param {Object} b The second comparison object ({key: bkey, value: bvalue}) + * @returns {number} 1 or -1, used in stringify cmp method + */ +function sortByKey(a, b) { + return a.key > b.key ? 1 : -1; +} + +//------------------------------------------------------------------------------ +// Private +//------------------------------------------------------------------------------ + +const CONFIG_FILES = [ + ".eslintrc.js", + ".eslintrc.yaml", + ".eslintrc.yml", + ".eslintrc.json", + ".eslintrc", + "package.json" +]; + +const resolver = new ModuleResolver(); + +/** + * Convenience wrapper for synchronously reading file contents. + * @param {string} filePath The filename to read. + * @returns {string} The file contents, with the BOM removed. + * @private + */ +function readFile(filePath) { + return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, ""); +} + +/** + * Determines if a given string represents a filepath or not using the same + * conventions as require(), meaning that the first character must be nonalphanumeric + * and not the @ sign which is used for scoped packages to be considered a file path. + * @param {string} filePath The string to check. + * @returns {boolean} True if it's a filepath, false if not. + * @private + */ +function isFilePath(filePath) { + return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0)); +} + +/** + * Loads a YAML configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadYAMLConfigFile(filePath) { + debug(`Loading YAML config file: ${filePath}`); + + // lazy load YAML to improve performance when not used + const yaml = require("js-yaml"); + + try { + + // empty YAML file can be null, so always use + return yaml.safeLoad(readFile(filePath)) || {}; + } catch (e) { + debug(`Error reading YAML file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a JSON configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadJSONConfigFile(filePath) { + debug(`Loading JSON config file: ${filePath}`); + + try { + return JSON.parse(stripComments(readFile(filePath))); + } catch (e) { + debug(`Error reading JSON file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a legacy (.eslintrc) configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadLegacyConfigFile(filePath) { + debug(`Loading config file: ${filePath}`); + + // lazy load YAML to improve performance when not used + const yaml = require("js-yaml"); + + try { + return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {}; + } catch (e) { + debug(`Error reading YAML file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a JavaScript configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadJSConfigFile(filePath) { + debug(`Loading JS config file: ${filePath}`); + try { + return requireUncached(filePath); + } catch (e) { + debug(`Error reading JavaScript file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a configuration from a package.json file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadPackageJSONConfigFile(filePath) { + debug(`Loading package.json config file: ${filePath}`); + try { + return loadJSONConfigFile(filePath).eslintConfig || null; + } catch (e) { + debug(`Error reading package.json file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Creates an error to notify about a missing config to extend from. + * @param {string} configName The name of the missing config. + * @returns {Error} The error object to throw + * @private + */ +function configMissingError(configName) { + const error = new Error(`Failed to load config "${configName}" to extend from.`); + + error.messageTemplate = "extend-config-missing"; + error.messageData = { + configName + }; + return error; +} + +/** + * Loads a configuration file regardless of the source. Inspects the file path + * to determine the correctly way to load the config file. + * @param {Object} file The path to the configuration. + * @returns {Object} The configuration information. + * @private + */ +function loadConfigFile(file) { + const filePath = file.filePath; + let config; + + switch (path.extname(filePath)) { + case ".js": + config = loadJSConfigFile(filePath); + if (file.configName) { + config = config.configs[file.configName]; + if (!config) { + throw configMissingError(file.configFullName); + } + } + break; + + case ".json": + if (path.basename(filePath) === "package.json") { + config = loadPackageJSONConfigFile(filePath); + if (config === null) { + return null; + } + } else { + config = loadJSONConfigFile(filePath); + } + break; + + case ".yaml": + case ".yml": + config = loadYAMLConfigFile(filePath); + break; + + default: + config = loadLegacyConfigFile(filePath); + } + + return ConfigOps.merge(ConfigOps.createEmptyConfig(), config); +} + +/** + * Writes a configuration file in JSON format. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @private + */ +function writeJSONConfigFile(config, filePath) { + debug(`Writing JSON config file: ${filePath}`); + + const content = stringify(config, { cmp: sortByKey, space: 4 }); + + fs.writeFileSync(filePath, content, "utf8"); +} + +/** + * Writes a configuration file in YAML format. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @private + */ +function writeYAMLConfigFile(config, filePath) { + debug(`Writing YAML config file: ${filePath}`); + + // lazy load YAML to improve performance when not used + const yaml = require("js-yaml"); + + const content = yaml.safeDump(config, { sortKeys: true }); + + fs.writeFileSync(filePath, content, "utf8"); +} + +/** + * Writes a configuration file in JavaScript format. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @private + */ +function writeJSConfigFile(config, filePath) { + debug(`Writing JS config file: ${filePath}`); + + const content = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`; + + fs.writeFileSync(filePath, content, "utf8"); +} + +/** + * Writes a configuration file. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @throws {Error} When an unknown file type is specified. + * @private + */ +function write(config, filePath) { + switch (path.extname(filePath)) { + case ".js": + writeJSConfigFile(config, filePath); + break; + + case ".json": + writeJSONConfigFile(config, filePath); + break; + + case ".yaml": + case ".yml": + writeYAMLConfigFile(config, filePath); + break; + + default: + throw new Error("Can't write to unknown file type."); + } +} + +/** + * Determines the base directory for node packages referenced in a config file. + * This does not include node_modules in the path so it can be used for all + * references relative to a config file. + * @param {string} configFilePath The config file referencing the file. + * @returns {string} The base directory for the file path. + * @private + */ +function getBaseDir(configFilePath) { + + // calculates the path of the project including ESLint as dependency + const projectPath = path.resolve(__dirname, "../../../"); + + if (configFilePath && pathIsInside(configFilePath, projectPath)) { + + // be careful of https://github.com/substack/node-resolve/issues/78 + return path.join(path.resolve(configFilePath)); + } + + /* + * default to ESLint project path since it's unlikely that plugins will be + * in this directory + */ + return path.join(projectPath); +} + +/** + * Determines the lookup path, including node_modules, for package + * references relative to a config file. + * @param {string} configFilePath The config file referencing the file. + * @returns {string} The lookup path for the file path. + * @private + */ +function getLookupPath(configFilePath) { + const basedir = getBaseDir(configFilePath); + + return path.join(basedir, "node_modules"); +} + +/** + * Resolves a eslint core config path + * @param {string} name The eslint config name. + * @returns {string} The resolved path of the config. + * @private + */ +function getEslintCoreConfigPath(name) { + if (name === "eslint:recommended") { + + /* + * Add an explicit substitution for eslint:recommended to + * conf/eslint-recommended.js. + */ + return path.resolve(__dirname, "../../conf/eslint-recommended.js"); + } + + if (name === "eslint:all") { + + /* + * Add an explicit substitution for eslint:all to conf/eslint-all.js + */ + return path.resolve(__dirname, "../../conf/eslint-all.js"); + } + + throw configMissingError(name); +} + +/** + * Applies values from the "extends" field in a configuration file. + * @param {Object} config The configuration information. + * @param {Config} configContext Plugin context for the config instance + * @param {string} filePath The file path from which the configuration information + * was loaded. + * @param {string} [relativeTo] The path to resolve relative to. + * @returns {Object} A new configuration object with all of the "extends" fields + * loaded and merged. + * @private + */ +function applyExtends(config, configContext, filePath, relativeTo) { + let configExtends = config.extends; + + // normalize into an array for easier handling + if (!Array.isArray(config.extends)) { + configExtends = [config.extends]; + } + + // Make the last element in an array take the highest precedence + config = configExtends.reduceRight((previousValue, parentPath) => { + try { + if (parentPath.startsWith("eslint:")) { + parentPath = getEslintCoreConfigPath(parentPath); + } else if (isFilePath(parentPath)) { + + /* + * If the `extends` path is relative, use the directory of the current configuration + * file as the reference point. Otherwise, use as-is. + */ + parentPath = (path.isAbsolute(parentPath) + ? parentPath + : path.join(relativeTo || path.dirname(filePath), parentPath) + ); + } + debug(`Loading ${parentPath}`); + + // eslint-disable-next-line no-use-before-define + return ConfigOps.merge(load(parentPath, configContext, relativeTo), previousValue); + } catch (e) { + + /* + * If the file referenced by `extends` failed to load, add the path + * to the configuration file that referenced it to the error + * message so the user is able to see where it was referenced from, + * then re-throw. + */ + e.message += `\nReferenced from: ${filePath}`; + throw e; + } + + }, config); + + return config; +} + +/** + * Resolves a configuration file path into the fully-formed path, whether filename + * or package name. + * @param {string} filePath The filepath to resolve. + * @param {string} [relativeTo] The path to resolve relative to. + * @returns {Object} An object containing 3 properties: + * - 'filePath' (required) the resolved path that can be used directly to load the configuration. + * - 'configName' the name of the configuration inside the plugin. + * - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'), + * or the absolute path to a config file. This should uniquely identify a config. + * @private + */ +function resolve(filePath, relativeTo) { + if (isFilePath(filePath)) { + const fullPath = path.resolve(relativeTo || "", filePath); + + return { filePath: fullPath, configFullName: fullPath }; + } + let normalizedPackageName; + + if (filePath.startsWith("plugin:")) { + const configFullName = filePath; + const pluginName = filePath.slice(7, filePath.lastIndexOf("/")); + const configName = filePath.slice(filePath.lastIndexOf("/") + 1); + + normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin"); + debug(`Attempting to resolve ${normalizedPackageName}`); + filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)); + return { filePath, configName, configFullName }; + } + normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config"); + debug(`Attempting to resolve ${normalizedPackageName}`); + filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)); + return { filePath, configFullName: filePath }; + + +} + +/** + * Loads a configuration file from the given file path. + * @param {Object} resolvedPath The value from calling resolve() on a filename or package name. + * @param {Config} configContext Plugins context + * @returns {Object} The configuration information. + */ +function loadFromDisk(resolvedPath, configContext) { + const dirname = path.dirname(resolvedPath.filePath), + lookupPath = getLookupPath(dirname); + let config = loadConfigFile(resolvedPath); + + if (config) { + + // ensure plugins are properly loaded first + if (config.plugins) { + configContext.plugins.loadAll(config.plugins); + } + + // include full path of parser if present + if (config.parser) { + if (isFilePath(config.parser)) { + config.parser = path.resolve(dirname || "", config.parser); + } else { + config.parser = resolver.resolve(config.parser, lookupPath); + } + } + + const ruleMap = configContext.linterContext.getRules(); + + // validate the configuration before continuing + validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments); + + /* + * If an `extends` property is defined, it represents a configuration file to use as + * a "parent". Load the referenced file and merge the configuration recursively. + */ + if (config.extends) { + config = applyExtends(config, configContext, resolvedPath.filePath, dirname); + } + } + + return config; +} + +/** + * Loads a config object, applying extends if present. + * @param {Object} configObject a config object to load + * @param {Config} configContext Context for the config instance + * @returns {Object} the config object with extends applied if present, or the passed config if not + * @private + */ +function loadObject(configObject, configContext) { + return configObject.extends ? applyExtends(configObject, configContext, "") : configObject; +} + +/** + * Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet + * cached. + * @param {string} filePath the path to the config file + * @param {Config} configContext Context for the config instance + * @param {string} [relativeTo] The path to resolve relative to. + * @returns {Object} the parsed config object (empty object if there was a parse error) + * @private + */ +function load(filePath, configContext, relativeTo) { + const resolvedPath = resolve(filePath, relativeTo); + + const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName); + + if (cachedConfig) { + return cachedConfig; + } + + const config = loadFromDisk(resolvedPath, configContext); + + if (config) { + config.filePath = resolvedPath.filePath; + config.baseDirectory = path.dirname(resolvedPath.filePath); + configContext.configCache.setConfig(resolvedPath.configFullName, config); + } + + return config; +} + + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + + getBaseDir, + getLookupPath, + load, + loadObject, + resolve, + write, + applyExtends, + CONFIG_FILES, + + /** + * Retrieves the configuration filename for a given directory. It loops over all + * of the valid configuration filenames in order to find the first one that exists. + * @param {string} directory The directory to check for a config file. + * @returns {?string} The filename of the configuration file for the directory + * or null if there is no configuration file in the directory. + */ + getFilenameForDirectory(directory) { + for (let i = 0, len = CONFIG_FILES.length; i < len; i++) { + const filename = path.join(directory, CONFIG_FILES[i]); + + if (fs.existsSync(filename) && fs.statSync(filename).isFile()) { + return filename; + } + } + + return null; + } +}; |