summaryrefslogtreecommitdiff
path: root/tools/node_modules/eslint/lib/config/config-file.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/node_modules/eslint/lib/config/config-file.js')
-rw-r--r--tools/node_modules/eslint/lib/config/config-file.js595
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;
+ }
+};