mirror of
https://github.com/nodejs/node.git
synced 2024-12-01 16:10:02 +01:00
d91e10b3bd
PR-URL: https://github.com/nodejs/io.js/pull/2072 Reviewed-By: Yosuke Furukawa <yosuke.furukawa@gmail.com> Reviewed-by: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Alex Kocharin <alex@kocharin.ru>
442 lines
14 KiB
JavaScript
442 lines
14 KiB
JavaScript
/**
|
|
* @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;
|