| "use strict"; |
| |
| var _ = require("underscore"); |
| var fs = require("fs"); |
| var cli = require("cli"); |
| var path = require("path"); |
| var shjs = require("shelljs"); |
| var minimatch = require("minimatch"); |
| var htmlparser = require("htmlparser2"); |
| var exit = require("exit"); |
| var stripJsonComments = require("strip-json-comments"); |
| 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], |
| "exclude-path": ["exclude-path", "Pass in a custom jshintignore file path", "string", null], |
| "filename": ["filename", |
| "Pass in a filename when using STDIN to emulate config lookup for that file name", |
| "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", ""], |
| |
| "extract": [ |
| "extract", |
| "Extract inline scripts contained in HTML (auto|always|never, default to never)", |
| "string", |
| "never" |
| ], |
| |
| // 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)"; |
| } |
| |
| /** |
| * 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 = getHomeDir(); |
| |
| if (!envs) |
| return home; |
| |
| 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; |
| } |
| |
| function getHomeDir() { |
| var homePath = ""; |
| var environment = global.process.env; |
| var paths = [ |
| environment.USERPROFILE, |
| environment.HOME, |
| environment.HOMEPATH, |
| environment.HOMEDRIVE + environment.HOMEPATH |
| ]; |
| |
| while (paths.length) { |
| homePath = paths.shift(); |
| if (fs.existsSync(homePath)) { |
| return homePath; |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| |
| try { |
| return require(fp).jshintConfig; |
| } catch (e) { |
| 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; |
| } |
| } |
| |
| // 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, cwd) { |
| cwd = cwd || process.cwd(); |
| |
| var filename = path.normalize(path.join(cwd, name)); |
| if (findFileResults[filename] !== undefined) { |
| return findFileResults[filename]; |
| } |
| |
| var parent = path.resolve(cwd, "../"); |
| |
| if (shjs.test("-e", filename)) { |
| findFileResults[filename] = filename; |
| return filename; |
| } |
| |
| if (cwd === 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(params) { |
| var file = findFile(params.excludePath || ".jshintignore", params.cwd); |
| |
| if (!file && !params.exclude) { |
| return []; |
| } |
| |
| var lines = (file ? shjs.cat(file) : "").split("\n"); |
| lines.unshift(params.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.join(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; |
| } |
| }); |
| } |
| |
| /** |
| * Extract JS code from a given source code. The source code my be either HTML |
| * code or JS code. In the latter case, no extraction will be done unless |
| * 'always' is given. |
| * |
| * @param {string} code a piece of code |
| * @param {string} when 'always' will extract the JS code, no matter what. |
| * 'never' won't do anything. 'auto' will check if the code looks like HTML |
| * before extracting it. |
| * |
| * @return {string} the extracted code |
| */ |
| function extract(code, when) { |
| // A JS file won't start with a less-than character, whereas a HTML file |
| // should always start with that. |
| if (when !== "always" && (when !== "auto" || !/^\s*</.test(code))) |
| return code; |
| |
| var inscript = false; |
| var index = 0; |
| var js = []; |
| var startOffset; |
| |
| // Test if current tag is a valid <script> tag. |
| function onopen(name, attrs) { |
| if (name !== "script") |
| return; |
| |
| if (attrs.type && !/text\/javascript/.test(attrs.type.toLowerCase())) |
| return; |
| |
| // Mark that we're inside a <script> a tag and push all new lines |
| // in between the last </script> tag and this <script> tag to preserve |
| // location information. |
| inscript = true; |
| js.push.apply(js, code.slice(index, parser.endIndex).match(/\n\r|\n|\r/g)); |
| startOffset = null; |
| } |
| |
| function onclose(name) { |
| if (name !== "script" || !inscript) |
| return; |
| |
| inscript = false; |
| index = parser.startIndex; |
| startOffset = null; |
| } |
| |
| function ontext(data) { |
| if (!inscript) |
| return; |
| |
| var lines = data.split(/\n\r|\n|\r/); |
| |
| if (!startOffset) { |
| lines.some(function (line) { |
| if (!line) return; |
| startOffset = /^(\s*)/.exec(line)[1]; |
| return true; |
| }); |
| } |
| |
| // check for startOffset again to remove leading white space from first line |
| if (startOffset) { |
| lines = lines.map(function (line) { |
| return line.replace(startOffset, ""); |
| }); |
| data = lines.join("\n"); |
| } |
| |
| js.push(data); // Collect JavaScript code. |
| } |
| |
| var parser = new htmlparser.Parser({ onopentag: onopen, onclosetag: onclose, ontext: ontext }); |
| parser.parseComplete(code); |
| |
| return js.join(""); |
| } |
| |
| /** |
| * Crude version of source maps: extract how much JavaSscript in HTML |
| * was shifted based on first JS line. For example if first js line |
| * is offset by 4 spaces, each line in this js fragment will have offset 4 |
| * to restore the original column. |
| * |
| * @param {string} code a piece of code |
| * @param {string} when 'always' will extract the JS code, no matter what. |
| * 'never' won't do anything. 'auto' will check if the code looks like HTML |
| * before extracting it. |
| * |
| * @return {Array} extracted offsets |
| */ |
| function extractOffsets(code, when) { |
| // A JS file won't start with a less-than character, whereas a HTML file |
| // should always start with that. |
| if (when !== "always" && (when !== "auto" || !/^\s*</.test(code))) |
| return; |
| |
| var inscript = false; |
| var index = 0; |
| var lineCounter = 0; |
| var startOffset; |
| var offsets = []; |
| |
| // Test if current tag is a valid <script> tag. |
| function onopen(name, attrs) { |
| if (name !== "script") |
| return; |
| |
| if (attrs.type && !/text\/javascript/.test(attrs.type.toLowerCase())) |
| return; |
| |
| // Mark that we're inside a <script> a tag and push all new lines |
| // in between the last </script> tag and this <script> tag to preserve |
| // location information. |
| inscript = true; |
| var fragment = code.slice(index, parser.endIndex); |
| var n = fragment.match(/\n\r|\n|\r/g).length; |
| lineCounter += n; |
| startOffset = null; |
| } |
| |
| function onclose(name) { |
| if (name !== "script" || !inscript) |
| return; |
| |
| inscript = false; |
| index = parser.startIndex; |
| startOffset = null; |
| } |
| |
| function ontext(data) { |
| if (!inscript) |
| return; |
| |
| var lines = data.split(/\n\r|\n|\r/); |
| |
| if (!startOffset) { |
| lines.some(function (line) { |
| if (!line) return; |
| startOffset = /^(\s*)/.exec(line)[1]; |
| return true; |
| }); |
| } |
| |
| // check for startOffset again to remove leading white space from first line |
| lines.forEach(function () { |
| lineCounter += 1; |
| if (startOffset) { |
| offsets[lineCounter] = startOffset.length; |
| } else { |
| offsets[lineCounter] = 0; |
| } |
| }); |
| } |
| |
| var parser = new htmlparser.Parser({ onopentag: onopen, onclosetag: onclose, ontext: ontext }); |
| parser.parseComplete(code); |
| return offsets; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| if (config.overrides) { |
| if (file) { |
| _.each(config.overrides, function (options, pattern) { |
| if (minimatch(path.normalize(file), pattern, { nocase: true, matchBase: true })) { |
| if (options.globals) { |
| globals = _.extend(globals || {}, options.globals); |
| delete options.globals; |
| } |
| _.extend(config, options); |
| } |
| }); |
| } |
| |
| delete config.overrides; |
| } |
| |
| 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 = { |
| extract: extract, |
| exit: exit, |
| |
| /** |
| * Returns a configuration file or nothing, if it can't be found. |
| */ |
| getConfig: function (fp) { |
| return loadNpmConfig(fp) || exports.loadConfig(findConfig(fp)); |
| }, |
| |
| /** |
| * 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); |
| exports.exit(1); |
| } |
| |
| try { |
| var config = JSON.parse(stripJsonComments(shjs.cat(fp))); |
| config.dirname = path.dirname(fp); |
| |
| if (config['extends']) { |
| var baseConfig = exports.loadConfig(path.resolve(config.dirname, config['extends'])); |
| config.globals = _.extend({}, baseConfig.globals, config.globals); |
| _.defaults(config, baseConfig); |
| delete config['extends']; |
| } |
| |
| return config; |
| } catch (err) { |
| cli.error("Can't parse config file: " + fp); |
| exports.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({cwd: opts.cwd}) : |
| 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) { |
| var config = opts.config; |
| var filename; |
| |
| // There is an if(filename) check in the lint() function called below. |
| // passing a filename of undefined is the same as calling the function |
| // without a filename. If there is no opts.filename, filename remains |
| // undefined and lint() is effectively called with 4 parameters. |
| if (opts.filename) { |
| filename = path.resolve(opts.filename); |
| } |
| |
| if (filename && !config) { |
| config = loadNpmConfig(filename) || |
| exports.loadConfig(findConfig(filename)); |
| } |
| |
| config = config || {}; |
| |
| lint(extract(code, opts.extract), results, config, data, filename); |
| (opts.reporter || defReporter)(results, data, { verbose: opts.verbose }); |
| cb(results.length === 0); |
| }); |
| |
| return null; |
| } |
| |
| files.forEach(function (file) { |
| var config = opts.config || exports.getConfig(file); |
| var code; |
| |
| try { |
| code = shjs.cat(file); |
| } catch (err) { |
| cli.error("Can't open " + file); |
| exports.exit(1); |
| } |
| |
| lint(extract(code, opts.extract), results, config, data, file); |
| |
| if (results.length) { |
| var offsets = extractOffsets(code, opts.extract); |
| if (offsets && offsets.length) { |
| results.forEach(function (errorInfo) { |
| var line = errorInfo.error.line; |
| if (line >= 0 && line < offsets.length) { |
| var offset = +offsets[line]; |
| errorInfo.error.character += offset; |
| } |
| }); |
| } |
| } |
| }); |
| |
| (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); |
| exports.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; |
| |
| exports.exit(passed ? 0 : 2); |
| } |
| |
| done(exports.run({ |
| args: cli.args, |
| config: config, |
| reporter: reporter, |
| ignores: loadIgnores({exclude: options.exclude, excludePath: options["exclude-path"]}), |
| extensions: options["extra-ext"], |
| verbose: options.verbose, |
| extract: options.extract, |
| filename: options.filename, |
| useStdin: {"-": true, "/dev/stdin": true}[args[args.length - 1]] |
| }, done)); |
| } |
| }; |
| |
| module.exports = exports; |