You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

441 lines
14 KiB

/**
* @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<string,*>} 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 || "<text>";
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;