blob: a34973331ce96aee895bc027ca068013e31a6982 [file] [log] [blame]
"use strict";
var cli = require("cli");
var path = require("path");
var shjs = require("shelljs");
var minimatch = require("minimatch");
var JSHINT = require("../stable/jshint.js").JSHINT;
var defReporter = require("../reporters/default").reporter;
var OPTIONS = {
"config": ["config", "Custom configuration file", "string", false ],
"reporter": ["reporter", "Custom reporter", "string", undefined ],
"verbose": ["verbose", "Show message codes"],
"show-non-errors": ["show-non-errors", "Show additional data generated by jshint"],
"extra-ext": ["extra-ext",
"Comma-separated list of file extensions to use (default is .js)", "string", ""],
// Deprecated options.
"jslint-reporter": [
"jslint-reporter",
deprecated("Use a jslint compatible reporter", "--reporter=jslint")
],
"checkstyle-reporter": [
"checkstyle-reporter",
deprecated("Use a CheckStyle compatible XML reporter", "--reporter=checkstyle")
]
};
/**
* Returns the same text but with a deprecation notice.
* Useful for options descriptions.
*
* @param {string} text
* @param {string} alt (optional) Alternative command to include in the
* deprecation notice.
*
* @returns {string}
*/
function deprecated(text, alt) {
if (!alt) {
return text + " (DEPRECATED)";
}
return text + " (DEPRECATED, use " + alt + " instead)";
}
/**
* Removes JavaScript comments from a string by replacing
* everything between block comments and everything after
* single-line comments in a non-greedy way.
*
* English version of the regex:
* match '/*'
* then match zero or more instances of any character (incl. \n)
* except for instances of '* /' (without a space, obv.)
* then match '* /' (again, without a space)
*
* @param {string} str a string with potential JavaScript comments.
* @returns {string} a string without JavaScript comments.
*/
function removeComments(str) {
str = str || "";
str = str.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\//g, "");
str = str.replace(/\/\/[^\n\r]*/g, ""); // Everything after '//'
return str;
}
/**
* Loads and parses a configuration file.
*
* @param {string} fp a path to the config file
* @returns {object} config object
*/
function loadConfig(fp) {
if (!fp) {
return {};
}
if (!shjs.test("-e", fp)) {
cli.error("Can't find config file: " + fp);
process.exit(1);
}
try {
return JSON.parse(removeComments(shjs.cat(fp)));
} catch (err) {
cli.error("Can't parse config file: " + fp);
process.exit(1);
}
}
/**
* Tries to find a configuration file in either project directory
* or in the home directory. Configuration files are named
* '.jshintrc'.
*
* @returns {string} a path to the config file
*/
function findConfig() {
var name = ".jshintrc";
var proj = findFile(name);
var home = path.normalize(path.join(process.env.HOME, name));
if (proj) {
return proj;
}
if (shjs.test("-e", home)) {
return home;
}
return null;
}
/**
* Tries to import a reporter file and returns its reference.
*
* @param {string} fp a path to the reporter file
* @returns {object} imported module for the reporter or 'null'
* if a module cannot be imported.
*/
function loadReporter(fp) {
try {
return require(fp).reporter;
} catch (err) {
return null;
}
}
/**
* Searches for a file with a specified name starting with
* 'dir' and going all the way up either until it finds the file
* or hits the root.
*
* @param {string} name filename to search for (e.g. .jshintrc)
* @param {string} dir directory to start search from (default:
* current working directory)
*
* @returns {string} normalized filename
*/
function findFile(name, dir) {
dir = dir || process.cwd();
var filename = path.normalize(path.join(dir, name));
var parent = path.resolve(dir, "../");
if (shjs.test("-e", filename)) {
return filename;
}
if (dir === parent) {
return null;
}
return findFile(name, parent);
}
/**
* Loads a list of files that have to be skipped. JSHint assumes that
* the list is located in a file called '.jshintignore'.
*
* @return {array} a list of files to ignore.
*/
function loadIgnores() {
var file = findFile(".jshintignore");
if (!file) {
return;
}
return shjs.cat(file).split("\n")
.filter(function (line) {
return !!line.trim();
})
.map(function (line) {
return path.resolve(path.dirname(file), line.trim());
});
}
/**
* Checks whether we should ignore a file or not.
*
* @param {string} fp a path to a file
* @param {array} patterns a list of patterns for files to ignore
*
* @return {boolean} 'true' if file should be ignored, 'false' otherwise.
*/
function isIgnored(fp, patterns) {
return patterns.some(function (ip) {
if (minimatch(path.resolve(fp), ip, { nocase: true })) {
return true;
}
if (path.resolve(fp) === ip) {
return true;
}
if (shjs.test("-d", fp) && ip.match(/^[^\/]*\/?$/) &&
fp.match(new RegExp("^" + ip + ".*"))) {
return true;
}
});
}
/**
* Recursively gather all files that need to be linted,
* excluding those that user asked to ignore.
*
* @param {string} fp a path to a file or directory to lint
* @param {array} files a pointer to an array that stores a list of files
* @param {array} ignores a list of patterns for files to ignore
* @param {array} ext a list of non-dot-js extensions to lint
*/
function collect(fp, files, ignores, ext) {
if (ignores && isIgnored(fp, ignores)) {
return;
}
if (!shjs.test("-e", fp)) {
cli.error("Can't open " + fp);
return;
}
if (shjs.test("-d", fp)) {
shjs.ls(fp).forEach(function (item) {
var itempath = path.join(fp, item);
if (shjs.test("-d", itempath) || item.match(ext)) {
collect(itempath, files, ignores, ext);
}
});
return;
}
files.push(fp);
}
/**
* Runs JSHint against provided file and saves the result
*
* @param {string} file a path to a file that needs to be linted
* @param {object} results a pointer to an object with results
* @param {object} config an object with JSHint configuration
* @param {object} data a pointer to an object with extra data
*/
function lint(file, results, config, data) {
var buffer;
var globals;
var lintData;
config = config || {};
config = JSON.parse(JSON.stringify(config));
try {
buffer = shjs.cat(file);
} catch (err) {
cli.error("Can't open " + file);
process.exit(1);
}
// Remove potential Unicode BOM.
buffer = buffer.replace(/^\uFEFF/, "");
if (config.globals) {
globals = config.globals;
delete config.globals;
}
if (!JSHINT(buffer, config, globals)) {
JSHINT.errors.forEach(function (err) {
if (err) {
results.push({ file: file, error: err });
}
});
}
lintData = JSHINT.data();
if (lintData) {
lintData.file = file;
data.push(lintData);
}
}
var exports = {
/**
* Gathers all files that need to be linted, lints them, sends them to
* a reporter and returns the overall result.
*
* @param {object} post-processed options from 'interpret':
* args - CLI arguments
* config - Configuration object
* reporter - Reporter function
* ignores - A list of files/dirs to ignore
* extensions - A list of non-dot-js extensions to check
*
* @returns {bool} 'true' if all files passed and 'false' otherwise.
*/
run: function (opts) {
var files = [];
var results = [];
var data = [];
var reg = new RegExp("\\.(js" +
(opts.extensions === "" ? "" : "|" +
opts.extensions.replace(/,/g, "|").replace(/[\. ]/g, "")) + ")$");
opts.args.forEach(function (target) {
collect(target, files, opts.ignores, reg);
});
files.forEach(function (file) {
lint(file, results, opts.config, data);
});
(opts.reporter || defReporter)(results, data, { verbose: opts.verbose });
return results.length === 0;
},
/**
* Main entrance function. Parses arguments and calls 'run' when
* its done. This function is called from bin/jshint file.
*
* @param {object} args, arguments in the process.argv format.
*/
interpret: function (args) {
cli.setArgv(args);
cli.options = {};
cli.enable("version", "glob", "help");
cli.setApp(path.resolve(__dirname + "/../../package.json"));
var options = cli.parse(OPTIONS);
var config = loadConfig(options.config || findConfig());
switch (true) {
// JSLint reporter
case options.reporter === "jslint":
case options["jslint-reporter"]:
options.reporter = "../reporters/jslint_xml.js";
break;
// CheckStyle (XML) reporter
case options.reporter === "checkstyle":
case options["checkstyle-reporter"]:
options.reporter = "../reporters/checkstyle.js";
break;
// Reporter that displays additional JSHint data
case options["show-non-errors"]:
options.reporter = "../reporters/non_error.js";
break;
// Custom reporter
case options.reporter !== undefined:
options.reporter = path.resolve(process.cwd(), options.reporter);
}
var reporter;
if (options.reporter) {
reporter = loadReporter(options.reporter);
if (reporter === null) {
cli.error("Can't load reporter file: " + options.reporter);
process.exit(1);
}
}
var passed = exports.run({
args: cli.args,
config: config,
reporter: reporter,
ignores: loadIgnores(),
extensions: options["extra-ext"],
verbose: options.verbose
});
// Avoid stdout cutoff in Node 0.4.x, also supports 0.5.x.
// See https://github.com/joyent/node/issues/1669
function exit() { process.exit(passed ? 0 : 2); }
try {
if (!process.stdout.flush()) {
process.stdout.once("drain", exit);
} else {
exit();
}
} catch (err) {
exit();
}
}
};
module.exports = exports;