/** * @fileoverview Main CLI object. * @author Nicholas C. Zakas * @copyright 2014 Nicholas C. Zakas. All rights reserved. */ "use strict"; /* * The CLI object should *not* call process.exit() directly. It should only return * exit codes. This allows other programs to use the CLI object and still control * when the program exits. */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ var fs = require("fs"), path = require("path"), assign = require("object-assign"), debug = require("debug"), rules = require("./rules"), eslint = require("./eslint"), traverse = require("./util/traverse"), IgnoredPaths = require("./ignored-paths"), Config = require("./config"), util = require("./util"), validator = require("./config-validator"); //------------------------------------------------------------------------------ // Typedefs //------------------------------------------------------------------------------ /** * The options to configure a CLI engine with. * @typedef {Object} CLIEngineOptions * @property {string} configFile The configuration file to use. * @property {boolean} reset True disables all default rules and environments. * @property {boolean|object} baseConfig Base config object. False disables all default rules and environments. * @property {boolean} ignore False disables use of .eslintignore. * @property {string[]} rulePaths An array of directories to load custom rules from. * @property {boolean} useEslintrc False disables looking for .eslintrc * @property {string[]} envs An array of environments to load. * @property {string[]} globals An array of global variables to declare. * @property {string[]} extensions An array of file extensions to check. * @property {Object} rules An object of rules to use. * @property {string} ignorePath The ignore file to use instead of .eslintignore. */ /** * A linting warning or error. * @typedef {Object} LintMessage * @property {string} message The message to display to the user. */ /** * A linting result. * @typedef {Object} LintResult * @property {string} filePath The path to the file that was linted. * @property {LintMessage[]} messages All of the messages for the result. */ //------------------------------------------------------------------------------ // Private //------------------------------------------------------------------------------ var defaultOptions = { configFile: null, reset: false, baseConfig: require(path.resolve(__dirname, "..", "conf", "eslint.json")), rulePaths: [], useEslintrc: true, envs: [], globals: [], rules: {}, extensions: [".js"], ignore: true, ignorePath: null }, loadedPlugins = Object.create(null); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ debug = debug("eslint:cli-engine"); /** * Load the given plugins if they are not loaded already. * @param {string[]} pluginNames An array of plugin names which should be loaded. * @returns {void} */ function loadPlugins(pluginNames) { if (pluginNames) { pluginNames.forEach(function (pluginName) { var pluginNamespace = util.getNamespace(pluginName), pluginNameWithoutNamespace = util.removeNameSpace(pluginName), pluginNameWithoutPrefix = util.removePluginPrefix(pluginNameWithoutNamespace), plugin; if (!loadedPlugins[pluginNameWithoutPrefix]) { debug("Load plugin " + pluginNameWithoutPrefix); plugin = require(pluginNamespace + util.PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix); // if this plugin has rules, import them if (plugin.rules) { rules.import(plugin.rules, pluginNameWithoutPrefix); } loadedPlugins[pluginNameWithoutPrefix] = plugin; } }); } } /** * It will calculate the error and warning count for collection of messages per file * @param {Object[]} messages - Collection of messages * @returns {Object} Contains the stats * @private */ function calculateStatsPerFile(messages) { return messages.reduce(function(stat, message) { if (message.fatal || message.severity === 2) { stat.errorCount++; } else { stat.warningCount++; } return stat; }, { errorCount: 0, warningCount: 0 }); } /** * It will calculate the error and warning count for collection of results from all files * @param {Object[]} results - Collection of messages from all the files * @returns {Object} Contains the stats * @private */ function calculateStatsPerRun(results) { return results.reduce(function(stat, result) { stat.errorCount += result.errorCount; stat.warningCount += result.warningCount; return stat; }, { errorCount: 0, warningCount: 0 }); } /** * Processes an source code using ESLint. * @param {string} text The source code to check. * @param {Object} configHelper The configuration options for ESLint. * @param {string} filename An optional string representing the texts filename. * @returns {Result} The results for linting on this text. * @private */ function processText(text, configHelper, filename) { // clear all existing settings for a new file eslint.reset(); var filePath, config, messages, stats, fileExtension = path.extname(filename), processor; if (filename) { filePath = path.resolve(filename); } filename = filename || ""; debug("Linting " + filename); config = configHelper.getConfig(filePath); loadPlugins(config.plugins); for (var plugin in loadedPlugins) { if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) { processor = loadedPlugins[plugin].processors[fileExtension]; break; } } if (processor) { var parsedBlocks = processor.preprocess(text, filename); var unprocessedMessages = []; parsedBlocks.forEach(function(block) { unprocessedMessages.push(eslint.verify(block, config, filename)); }); messages = processor.postprocess(unprocessedMessages, filename); } else { messages = eslint.verify(text, config, filename); } stats = calculateStatsPerFile(messages); return { filePath: filename, messages: messages, errorCount: stats.errorCount, warningCount: stats.warningCount }; } /** * Processes an individual file using ESLint. Files used here are known to * exist, so no need to check that here. * @param {string} filename The filename of the file being checked. * @param {Object} configHelper The configuration options for ESLint. * @returns {Result} The results for linting on this file. * @private */ function processFile(filename, configHelper) { var text = fs.readFileSync(path.resolve(filename), "utf8"); return processText(text, configHelper, filename); } /** * Returns result with warning by ignore settings * @param {string} filePath File path of checked code * @returns {Result} Result with single warning * @private */ function createIgnoreResult(filePath) { return { filePath: filePath, messages: [ { fatal: false, severity: 1, message: "File ignored because of your .eslintignore file. Use --no-ignore to override." } ], errorCount: 0, warningCount: 1 }; } //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * Creates a new instance of the core CLI engine. * @param {CLIEngineOptions} options The options for this instance. * @constructor */ function CLIEngine(options) { /** * Stored options for this instance * @type {Object} */ this.options = assign(Object.create(defaultOptions), options || {}); // load in additional rules if (this.options.rulePaths) { this.options.rulePaths.forEach(function(rulesdir) { debug("Loading rules from " + rulesdir); rules.load(rulesdir); }); } Object.keys(this.options.rules || {}).forEach(function(name) { validator.validateRuleOptions(name, this.options.rules[name], "CLI"); }.bind(this)); } CLIEngine.prototype = { constructor: CLIEngine, /** * Add a plugin by passing it's configuration * @param {string} name Name of the plugin. * @param {Object} pluginobject Plugin configuration object. * @returns {void} */ addPlugin: function(name, pluginobject) { var pluginNameWithoutPrefix = util.removePluginPrefix(util.removeNameSpace(name)); if (pluginobject.rules) { rules.import(pluginobject.rules, pluginNameWithoutPrefix); } loadedPlugins[pluginNameWithoutPrefix] = pluginobject; }, /** * Executes the current configuration on an array of file and directory names. * @param {string[]} files An array of file and directory names. * @returns {Object} The results for all files that were linted. */ executeOnFiles: function(files) { var results = [], processed = [], options = this.options, configHelper = new Config(options), ignoredPaths = IgnoredPaths.load(options), exclude = ignoredPaths.contains.bind(ignoredPaths), stats; traverse({ files: files, extensions: options.extensions, exclude: options.ignore ? exclude : false }, function(filename) { debug("Processing " + filename); processed.push(filename); results.push(processFile(filename, configHelper)); }); // only warn for files explicitly passed on the command line if (options.ignore) { files.forEach(function(file) { if (fs.statSync(path.resolve(file)).isFile() && processed.indexOf(file) === -1) { results.push(createIgnoreResult(file)); } }); } stats = calculateStatsPerRun(results); return { results: results, errorCount: stats.errorCount, warningCount: stats.warningCount }; }, /** * Executes the current configuration on text. * @param {string} text A string of JavaScript code to lint. * @param {string} filename An optional string representing the texts filename. * @returns {Object} The results for the linting. */ executeOnText: function(text, filename) { var results = [], stats, options = this.options, configHelper = new Config(options), ignoredPaths = IgnoredPaths.load(options), exclude = ignoredPaths.contains.bind(ignoredPaths); if (filename && options.ignore && exclude(filename)) { results.push(createIgnoreResult(filename)); } else { results.push(processText(text, configHelper, filename)); } stats = calculateStatsPerRun(results); return { results: results, errorCount: stats.errorCount, warningCount: stats.warningCount }; }, /** * Returns a configuration object for the given file based on the CLI options. * This is the same logic used by the ESLint CLI executable to determine * configuration for each file it processes. * @param {string} filePath The path of the file to retrieve a config object for. * @returns {Object} A configuration object for the file. */ getConfigForFile: function(filePath) { var configHelper = new Config(this.options); return configHelper.getConfig(filePath); }, /** * Checks if a given path is ignored by ESLint. * @param {string} filePath The path of the file to check. * @returns {boolean} Whether or not the given path is ignored. */ isPathIgnored: function (filePath) { var ignoredPaths; if (this.options.ignore) { ignoredPaths = IgnoredPaths.load(this.options); return ignoredPaths.contains(filePath); } return false; }, /** * Returns the formatter representing the given format or null if no formatter * with the given name can be found. * @param {string} [format] The name of the format to load or the path to a * custom formatter. * @returns {Function} The formatter function or null if not found. */ getFormatter: function(format) { var formatterPath; // default is stylish format = format || "stylish"; // only strings are valid formatters if (typeof format === "string") { // replace \ with / for Windows compatibility format = format.replace(/\\/g, "/"); // if there's a slash, then it's a file if (format.indexOf("/") > -1) { formatterPath = path.resolve(process.cwd(), format); } else { formatterPath = "./formatters/" + format; } try { return require(formatterPath); } catch (ex) { return null; } } else { return null; } } }; module.exports = CLIEngine;