mirror of
https://github.com/jsdoc/jsdoc.git
synced 2026-01-25 15:24:34 +00:00
507 lines
13 KiB
JavaScript
507 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
var cli = require("cli");
|
|
var path = require("path");
|
|
var shjs = require("shelljs");
|
|
var minimatch = require("minimatch");
|
|
var JSHINT = require("./jshint.js").JSHINT;
|
|
var defReporter = require("./reporters/default").reporter;
|
|
|
|
var OPTIONS = {
|
|
"config": ["c", "Custom configuration file", "string", false ],
|
|
"reporter": ["reporter", "Custom reporter (<PATH>|jslint|checkstyle)", "string", undefined ],
|
|
"exclude": ["exclude",
|
|
"Exclude files matching the given filename pattern (same as .jshintignore)", "string", null],
|
|
"verbose": ["verbose", "Show message codes"],
|
|
"show-non-errors": ["show-non-errors", "Show additional data generated by jshint"],
|
|
"extra-ext": ["e",
|
|
"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;
|
|
}
|
|
|
|
/**
|
|
* Tries to find a configuration file in either project directory
|
|
* or in the home directory. Configuration files are named
|
|
* '.jshintrc'.
|
|
*
|
|
* @param {string} file path to the file to be linted
|
|
* @returns {string} a path to the config file
|
|
*/
|
|
function findConfig(file) {
|
|
var dir = path.dirname(path.resolve(file));
|
|
var envs = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
|
var home = path.normalize(path.join(envs, ".jshintrc"));
|
|
|
|
var proj = findFile(".jshintrc", dir);
|
|
if (proj)
|
|
return proj;
|
|
|
|
if (shjs.test("-e", home))
|
|
return home;
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Tries to find JSHint configuration within a package.json file
|
|
* (if any). It search in the current directory and then goes up
|
|
* all the way to the root just like findFile.
|
|
*
|
|
* @param {string} file path to the file to be linted
|
|
* @returns {object} config object
|
|
*/
|
|
function loadNpmConfig(file) {
|
|
var dir = path.dirname(path.resolve(file));
|
|
var fp = findFile("package.json", dir);
|
|
|
|
if (!fp)
|
|
return null;
|
|
|
|
return require(fp).jshintConfig || 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;
|
|
}
|
|
}
|
|
|
|
// Storage for memoized results from find file
|
|
// Should prevent lots of directory traversal &
|
|
// lookups when liniting an entire project
|
|
var findFileResults = {};
|
|
|
|
/**
|
|
* 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));
|
|
if (findFileResults[filename] !== undefined) {
|
|
return findFileResults[filename];
|
|
}
|
|
|
|
var parent = path.resolve(dir, "../");
|
|
|
|
if (shjs.test("-e", filename)) {
|
|
findFileResults[filename] = filename;
|
|
return filename;
|
|
}
|
|
|
|
if (dir === parent) {
|
|
findFileResults[filename] = null;
|
|
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(exclude) {
|
|
var file = findFile(".jshintignore");
|
|
|
|
if (!file && !exclude) {
|
|
return [];
|
|
}
|
|
|
|
var lines = (file ? shjs.cat(file) : "").split("\n");
|
|
lines.unshift(exclude || "");
|
|
|
|
return lines
|
|
.filter(function (line) {
|
|
return !!line.trim();
|
|
})
|
|
.map(function (line) {
|
|
if (line[0] === "!")
|
|
return "!" + path.resolve(path.dirname(file), line.substr(1).trim());
|
|
|
|
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} code code 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
|
|
* @param {string} file (optional) file name that is being linted
|
|
*/
|
|
function lint(code, results, config, data, file) {
|
|
var globals;
|
|
var lintData;
|
|
var buffer = [];
|
|
|
|
config = config || {};
|
|
config = JSON.parse(JSON.stringify(config));
|
|
|
|
if (config.prereq) {
|
|
config.prereq.forEach(function (fp) {
|
|
fp = path.join(config.dirname, fp);
|
|
if (shjs.test("-e", fp))
|
|
buffer.push(shjs.cat(fp));
|
|
});
|
|
delete config.prereq;
|
|
}
|
|
|
|
if (config.globals) {
|
|
globals = config.globals;
|
|
delete config.globals;
|
|
}
|
|
|
|
delete config.dirname;
|
|
buffer.push(code);
|
|
buffer = buffer.join("\n");
|
|
buffer = buffer.replace(/^\uFEFF/, ""); // Remove potential Unicode BOM.
|
|
|
|
if (!JSHINT(buffer, config, globals)) {
|
|
JSHINT.errors.forEach(function (err) {
|
|
if (err) {
|
|
results.push({ file: file || "stdin", error: err });
|
|
}
|
|
});
|
|
}
|
|
|
|
lintData = JSHINT.data();
|
|
|
|
if (lintData) {
|
|
lintData.file = file || "stdin";
|
|
data.push(lintData);
|
|
}
|
|
}
|
|
|
|
var exports = {
|
|
/**
|
|
* Loads and parses a configuration file.
|
|
*
|
|
* @param {string} fp a path to the config file
|
|
* @returns {object} config object
|
|
*/
|
|
loadConfig: function (fp) {
|
|
if (!fp) {
|
|
return {};
|
|
}
|
|
|
|
if (!shjs.test("-e", fp)) {
|
|
cli.error("Can't find config file: " + fp);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
var config = JSON.parse(removeComments(shjs.cat(fp)));
|
|
config.dirname = path.dirname(fp);
|
|
return config;
|
|
} catch (err) {
|
|
cli.error("Can't parse config file: " + fp);
|
|
process.exit(1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gathers all files that need to be linted
|
|
*
|
|
* @param {object} post-processed options from 'interpret':
|
|
* args - CLI arguments
|
|
* ignores - A list of files/dirs to ignore (defaults to .jshintignores)
|
|
* extensions - A list of non-dot-js extensions to check
|
|
*/
|
|
gather: function (opts) {
|
|
var files = [];
|
|
|
|
var reg = new RegExp("\\.(js" +
|
|
(!opts.extensions ? "" : "|" +
|
|
opts.extensions.replace(/,/g, "|").replace(/[\. ]/g, "")) + ")$");
|
|
|
|
var ignores = !opts.ignores ? loadIgnores() : opts.ignores.map(function (target) {
|
|
return path.resolve(target);
|
|
});
|
|
|
|
opts.args.forEach(function (target) {
|
|
collect(target, files, ignores, reg);
|
|
});
|
|
|
|
return files;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* @param {function} cb a callback to call when function is finished
|
|
* asynchronously.
|
|
*
|
|
* @returns {bool} 'true' if all files passed, 'false' otherwise and 'null'
|
|
* when function will be finished asynchronously.
|
|
*/
|
|
run: function (opts, cb) {
|
|
var files = exports.gather(opts);
|
|
var results = [];
|
|
var data = [];
|
|
|
|
if (opts.useStdin) {
|
|
cli.withStdin(function (code) {
|
|
lint(code, results, opts.config || {}, data);
|
|
(opts.reporter || defReporter)(results, data, { verbose: opts.verbose });
|
|
cb(results.length === 0);
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
files.forEach(function (file) {
|
|
var config = opts.config || loadNpmConfig(file) || exports.loadConfig(findConfig(file));
|
|
var code;
|
|
|
|
try {
|
|
code = shjs.cat(file);
|
|
} catch (err) {
|
|
cli.error("Can't open " + file);
|
|
process.exit(1);
|
|
}
|
|
|
|
lint(code, results, config, data, file);
|
|
});
|
|
|
|
(opts.reporter || defReporter)(results, data, { verbose: opts.verbose });
|
|
return results.length === 0;
|
|
},
|
|
|
|
/**
|
|
* Helper exposed for testing.
|
|
* Used to determine is stdout has any buffered output before exiting the program
|
|
*/
|
|
getBufferSize: function () {
|
|
return process.stdout.bufferSize;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
// Use config file if specified
|
|
var config;
|
|
if (options.config) {
|
|
config = exports.loadConfig(options.config);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// This is a hack. exports.run is both sync and async function
|
|
// because I needed stdin support (and cli.withStdin is async)
|
|
// and was too lazy to change tests.
|
|
|
|
function done(passed) {
|
|
/*jshint eqnull:true */
|
|
|
|
if (passed == null)
|
|
return;
|
|
|
|
// Patch as per https://github.com/visionmedia/mocha/issues/333
|
|
// fixes issues with piped output on Windows.
|
|
// Root issue is here https://github.com/joyent/node/issues/3584
|
|
function exit() { process.exit(passed ? 0 : 2); }
|
|
try {
|
|
if (exports.getBufferSize()) {
|
|
process.stdout.once('drain', exit);
|
|
} else {
|
|
exit();
|
|
}
|
|
} catch (err) {
|
|
exit();
|
|
}
|
|
}
|
|
|
|
done(exports.run({
|
|
args: cli.args,
|
|
config: config,
|
|
reporter: reporter,
|
|
ignores: loadIgnores(options.exclude),
|
|
extensions: options["extra-ext"],
|
|
verbose: options.verbose,
|
|
useStdin: {"-": true, "/dev/stdin": true}[args[args.length - 1]]
|
|
}, done));
|
|
}
|
|
};
|
|
|
|
module.exports = exports;
|