diff --git a/LICENSE.md b/LICENSE.md index 7f78fa71..43565bb2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -261,6 +261,21 @@ The source code for Node.js is available at: https://github.com/joyent/node +## node-browser-builtins ## + +Portions of the node-browser-builtins source code are incorporated into the +following files: + +- `rhino/assert.js` +- `rhino/rhino-shim.js` + +node-browser-builtins is distributed under the MIT license, which is reproduced +above. + +The source code for node-browser-builtins is available at: +https://github.com/alexgorbatchev/node-browser-builtins + + ## node-browserify ## Portions of the node-browserify source code are incorporated into the following diff --git a/cli.js b/cli.js index 10690e0a..20cf0e55 100644 --- a/cli.js +++ b/cli.js @@ -3,32 +3,39 @@ * * A few critical notes for anyone who works on this module: * - * + The module should really export an instance of `JSDoc`, and `props` should be properties of a - * `JSDoc` instance. However, Rhino interpreted `this` as a reference to `global` within the + * + The module should really export an instance of `cli`, and `props` should be properties of a + * `cli` instance. However, Rhino interpreted `this` as a reference to `global` within the * prototype's methods, so we couldn't do that. - * + Use the `fs` and `path` modules rather than `jsdoc/fs` and `jsdoc/path`, which are not - * initialized correctly when they're loaded this early. + * + On Rhino, for unknown reasons, the `jsdoc/fs` and `jsdoc/path` modules can fail in some cases + * when they are required by this module. You may need to use `fs` and `path` instead. * * @private */ module.exports = (function() { 'use strict'; +var logger = require('jsdoc/util/logger'); + var props = { docs: [], + shouldExitWithError: false, packageJson: null }; var app = global.app; var env = global.env; -var JSDoc = {}; +var fatalErrorMessage = 'Exiting JSDoc because an error occurred. See the previous log ' + + 'messages for details.'; + +var cli = {}; // TODO: docs -JSDoc.setVersionInfo = function() { +cli.setVersionInfo = function() { var fs = require('fs'); var path = require('path'); + // allow this to throw--something is really wrong if we can't read our own package file var info = JSON.parse( fs.readFileSync(path.join(env.dirname, 'package.json'), 'utf8') ); env.version = { @@ -36,11 +43,11 @@ JSDoc.setVersionInfo = function() { revision: new Date( parseInt(info.revision, 10) ).toUTCString() }; - return JSDoc; + return cli; }; // TODO: docs -JSDoc.loadConfig = function() { +cli.loadConfig = function() { var _ = require('underscore'); var args = require('jsdoc/opts/args'); var Config = require('jsdoc/config'); @@ -55,7 +62,12 @@ JSDoc.loadConfig = function() { encoding: 'utf8' }; - env.opts = args.parse(env.args); + try { + env.opts = args.parse(env.args); + } + catch (e) { + cli.exit(1, e.message + '\n' + fatalErrorMessage); + } confPath = env.opts.configure || path.join(env.dirname, 'conf.json'); try { @@ -74,48 +86,109 @@ JSDoc.loadConfig = function() { .get(); } catch (e) { - throw new Error('Cannot parse the config file ' + confPath + ': ' + e); + cli.exit(1, 'Cannot parse the config file ' + confPath + ': ' + e + '\n' + + fatalErrorMessage); } // look for options on the command line, in the config file, and in the defaults, in that order env.opts = _.defaults(env.opts, env.conf.opts, defaultOpts); - return JSDoc; + return cli; }; // TODO: docs -JSDoc.runCommand = function(cb) { +cli.configureLogger = function() { + function recoverableError() { + props.shouldExitWithError = true; + } + + function fatalError() { + cli.exit(1, fatalErrorMessage); + } + + if (env.opts.debug) { + logger.setLevel(logger.LEVELS.DEBUG); + } + else if (env.opts.verbose) { + logger.setLevel(logger.LEVELS.INFO); + } + + if (env.opts.pedantic) { + logger.once('logger:warn', recoverableError); + logger.once('logger:error', fatalError); + } + else { + logger.once('logger:error', recoverableError); + } + + logger.once('logger:fatal', fatalError); + + return cli; +}; + +// TODO: docs +cli.logStart = function() { + var loggerFunc = env.opts.help ? console.log : logger.info; + cli.printVersion(loggerFunc); + + logger.debug('Environment info: {"env":{"conf":%j,"opts":%j}}', env.conf, env.opts); +}; + +// TODO: docs +cli.logFinish = function() { + var delta; + var deltaSeconds; + + if (env.run.finish && env.run.start) { + delta = env.run.finish.getTime() - env.run.start.getTime(); + } + + if (delta !== undefined) { + deltaSeconds = (delta / 1000).toFixed(2); + logger.info('Finished running in %s seconds.', deltaSeconds); + } +}; + +// TODO: docs +cli.runCommand = function(cb) { var cmd; var opts = env.opts; + function done(errorCode) { + if (!errorCode && props.shouldExitWithError) { + cb(1); + } + else { + cb(errorCode); + } + } + if (opts.help) { - cmd = JSDoc.printHelp; + cmd = cli.printHelp; } else if (opts.test) { - cmd = JSDoc.runTests; + cmd = cli.runTests; } else if (opts.version) { - cmd = JSDoc.printVersion; + cmd = function(callback) { callback(); }; } else { - cmd = JSDoc.main; + cmd = cli.main; } - cmd(cb); + cmd(done); }; // TODO: docs -JSDoc.printHelp = function(cb) { - JSDoc.printVersion(function() { - console.log( '\n' + require('jsdoc/opts/args').help() ); - console.log('\n' + 'Visit http://usejsdoc.org for more information.'); - cb(0); - }); +cli.printHelp = function(cb) { + console.log( '\n' + require('jsdoc/opts/args').help() + '\n' ); + console.log('Visit http://usejsdoc.org for more information.'); + cb(0); }; // TODO: docs -JSDoc.runTests = function(cb) { +cli.runTests = function(cb) { var path = require('jsdoc/path'); var runner = require( path.join(env.dirname, 'test/runner') ); @@ -127,17 +200,26 @@ JSDoc.runTests = function(cb) { }; // TODO: docs -JSDoc.printVersion = function(cb) { - console.log('JSDoc %s (%s)', env.version.number, env.version.revision); - cb(0); +cli.getVersion = function() { + return 'JSDoc ' + env.version.number + ' (' + env.version.revision + ')'; }; // TODO: docs -JSDoc.main = function(cb) { - JSDoc.scanFiles(); +cli.printVersion = function(loggerFunc, cb) { + loggerFunc = loggerFunc || logger.info; + + loggerFunc.call( null, cli.getVersion() ); + if (cb) { + cb(0); + } +}; + +// TODO: docs +cli.main = function(cb) { + cli.scanFiles(); if (env.sourceFiles.length) { - JSDoc.createParser() + cli.createParser() .parseFiles() .processParseResults(); } @@ -146,7 +228,8 @@ JSDoc.main = function(cb) { cb(0); }; -JSDoc.scanFiles = function() { +// TODO: docs +cli.scanFiles = function() { var Filter = require('jsdoc/src/filter').Filter; var fs = require('jsdoc/fs'); var Readme = require('jsdoc/readme'); @@ -181,10 +264,10 @@ JSDoc.scanFiles = function() { filter); } - return JSDoc; + return cli; }; -JSDoc.createParser = function() { +cli.createParser = function() { var handlers = require('jsdoc/src/handlers'); var parser = require('jsdoc/src/parser'); var plugins = require('jsdoc/plugins'); @@ -197,10 +280,10 @@ JSDoc.createParser = function() { handlers.attachTo(app.jsdoc.parser); - return JSDoc; + return cli; }; -JSDoc.parseFiles = function() { +cli.parseFiles = function() { var augment = require('jsdoc/augment'); var borrow = require('jsdoc/borrow'); var Package = require('jsdoc/package').Package; @@ -216,40 +299,46 @@ JSDoc.parseFiles = function() { packageDocs.files = env.sourceFiles || []; docs.push(packageDocs); + logger.debug('Adding inherited symbols...'); borrow.indexAll(docs); - augment.addInherited(docs); borrow.resolveBorrows(docs); app.jsdoc.parser.fireProcessingComplete(docs); - return JSDoc; + return cli; }; -JSDoc.processParseResults = function() { +cli.processParseResults = function() { if (env.opts.explain) { - JSDoc.dumpParseResults(); + cli.dumpParseResults(); } else { - JSDoc.resolveTutorials(); - JSDoc.generateDocs(); + cli.resolveTutorials(); + cli.generateDocs(); } + + return cli; }; -JSDoc.dumpParseResults = function() { +cli.dumpParseResults = function() { global.dump(props.docs); + + return cli; }; -JSDoc.resolveTutorials = function() { +cli.resolveTutorials = function() { var resolver = require('jsdoc/tutorial/resolver'); if (env.opts.tutorials) { resolver.load(env.opts.tutorials); resolver.resolve(); } + + return cli; }; -JSDoc.generateDocs = function() { +cli.generateDocs = function() { var path = require('jsdoc/path'); var resolver = require('jsdoc/tutorial/resolver'); var taffy = require('taffydb').taffy; @@ -266,26 +355,38 @@ JSDoc.generateDocs = function() { template = require(env.opts.template + '/publish'); } catch(e) { - throw new Error('Unable to load template: ' + e.message || e); + logger.fatal('Unable to load template: ' + e.message || e); } // templates should include a publish.js file that exports a "publish" function if (template.publish && typeof template.publish === 'function') { // convert this from a URI back to a path if necessary env.opts.template = path._uriToPath(env.opts.template); + logger.printInfo('Generating output files...'); template.publish( taffy(props.docs), env.opts, resolver.root ); + logger.info('complete.'); } else { - throw new Error(env.opts.template + ' does not export a "publish" function. Global ' + + logger.fatal(env.opts.template + ' does not export a "publish" function. Global ' + '"publish" functions are no longer supported.'); } + return cli; }; -return JSDoc; +// TODO: docs +cli.exit = function(exitCode, errorMessage) { + if (errorMessage) { + logger.fatal(errorMessage); + } + + process.exit(exitCode || 0); +}; + +return cli; })(); diff --git a/jsdoc b/jsdoc index a1d5a611..e82d6802 100755 --- a/jsdoc +++ b/jsdoc @@ -28,10 +28,7 @@ ENCODEDBASEPATH=`echo "$BASEPATH" | sed -e 's/ /%20/g'` if test "$1" = "--debug" then - echo "Running Debug" CMD="org.mozilla.javascript.tools.debugger.Main -debug -opt -1" - # strip --debug argument - shift else CMD="org.mozilla.javascript.tools.shell.Main" fi diff --git a/jsdoc.cmd b/jsdoc.cmd index 3a21ac67..d7adca4c 100644 --- a/jsdoc.cmd +++ b/jsdoc.cmd @@ -25,18 +25,10 @@ IF NOT "%_URLPATH%"=="%_URLPATH: =%" GOTO ESCAPE_SPACE IF [%1]==[--debug] ( ECHO Running Debug SET CMD=org.mozilla.javascript.tools.debugger.Main -debug -opt -1 - - REM `SHIFT` doesn't affect %* - :COLLECT_ARGS - IF [%2]==[] GOTO LAST_ARG - SET ARGS=%ARGS% %2 - SHIFT - GOTO COLLECT_ARGS ) ELSE ( SET CMD=org.mozilla.javascript.tools.shell.Main - SET ARGS=%* ) -:LAST_ARG +SET ARGS=%* IF [%1]==[-T] ( java -classpath "%_BASEPATH%/rhino/js.jar" %CMD% -opt -1 -modules "%_URLPATH%/lib" -modules "%_URLPATH%/node_modules" -modules "%_URLPATH%/rhino" -modules "%_URLPATH%" "%_BASEPATH%/jsdoc.js" %ARGS% --nocolor --dirname="%_BASEPATH%/ diff --git a/jsdoc.js b/jsdoc.js index 4c9a6e27..33ecdfdd 100755 --- a/jsdoc.js +++ b/jsdoc.js @@ -126,10 +126,7 @@ global.dump = function() { (function() { 'use strict'; - function cb(errorCode) { - process.exit(errorCode || 0); - } - + var logger = require('jsdoc/util/logger'); var path = require('jsdoc/path'); var runtime = require('jsdoc/util/runtime'); @@ -138,6 +135,17 @@ global.dump = function() { cli.setVersionInfo() .loadConfig(); + if (!global.env.opts.test) { + cli.configureLogger(); + } + + cli.logStart(); + + function cb(errorCode) { + cli.logFinish(); + cli.exit(errorCode || 0); + } + // On Rhino, we use a try/catch block so we can log the Java exception (if available) if ( runtime.isRhino() ) { try { @@ -145,11 +153,10 @@ global.dump = function() { } catch(e) { if (e.rhinoException) { - e.rhinoException.printStackTrace(); - process.exit(1); + logger.fatal( e.rhinoException.printStackTrace() ); } else { console.trace(e); - process.exit(1); + cli.exit(1); } } } diff --git a/lib/jsdoc/borrow.js b/lib/jsdoc/borrow.js index 7c3d438f..fc0b606d 100644 --- a/lib/jsdoc/borrow.js +++ b/lib/jsdoc/borrow.js @@ -6,7 +6,8 @@ */ 'use strict'; -var doop = require("jsdoc/util/doop").doop; +var doop = require('jsdoc/util/doop'); +var logger = require('jsdoc/util/logger'); var hasOwnProp = Object.prototype.hasOwnProperty; @@ -31,7 +32,8 @@ exports.indexAll = function(docs) { */ exports.resolveBorrows = function(docs) { if (!docs.index) { - throw 'Docs has not been indexed: docs.index must be defined here.'; + logger.error('Unable to resolve borrowed symbols, because the docs have not been indexed.'); + return; } docs.forEach(function(doc) { diff --git a/lib/jsdoc/opts/argparser.js b/lib/jsdoc/opts/argparser.js index caf1f7e6..557b65ee 100644 --- a/lib/jsdoc/opts/argparser.js +++ b/lib/jsdoc/opts/argparser.js @@ -1,8 +1,8 @@ /** - Parse the command line arguments. - @module jsdoc/opts/argparser - @author Michael Mathews - @license Apache License 2.0 - See file 'LICENSE.md' in this project. + * Parse the command line arguments. + * @module jsdoc/opts/argparser + * @author Michael Mathews + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. */ 'use strict'; @@ -11,29 +11,45 @@ var _ = require('underscore'); var hasOwnProp = Object.prototype.hasOwnProperty; /** - Create an instance of the parser. - @classdesc A parser to interpret the key-value pairs entered on the command - line. - @constructor + * Create an instance of the parser. + * @classdesc A parser to interpret the key-value pairs entered on the command line. + * @constructor */ var ArgParser = function() { - this._options = []; - this._shortNameIndex = {}; - this._longNameIndex = {}; + this._options = []; + this._shortNameIndex = {}; + this._longNameIndex = {}; }; ArgParser.prototype._getOptionByShortName = function(name) { - if (hasOwnProp.call(this._shortNameIndex, name)) { - return this._options[this._shortNameIndex[name]]; - } - return null; + if (hasOwnProp.call(this._shortNameIndex, name)) { + return this._options[this._shortNameIndex[name]]; + } + return null; }; ArgParser.prototype._getOptionByLongName = function(name) { - if (hasOwnProp.call(this._longNameIndex, name)) { - return this._options[this._longNameIndex[name]]; - } - return null; + if (hasOwnProp.call(this._longNameIndex, name)) { + return this._options[this._longNameIndex[name]]; + } + return null; +}; + +ArgParser.prototype._addOption = function(option) { + var currentIndex; + + var longName = option.longName; + var shortName = option.shortName; + + this._options.push(option); + currentIndex = this._options.length - 1; + + if (shortName) { + this._shortNameIndex[shortName] = currentIndex; + } + if (longName) { + this._longNameIndex[longName] = currentIndex; + } }; /** @@ -49,209 +65,234 @@ ArgParser.prototype._getOptionByLongName = function(name) { * myParser.addOption('h', 'help', false, 'Show the help message.'); */ ArgParser.prototype.addOption = function(shortName, longName, hasValue, helpText, canHaveMultiple, coercer) { - this._options.push({ - shortName: shortName, - longName: longName, - hasValue: hasValue, - helpText: helpText, - canHaveMultiple: (canHaveMultiple || false), - coercer: coercer - }); - - if (shortName) { - this._shortNameIndex[shortName] = this._options.length - 1; - } - if (longName) { - this._longNameIndex[longName] = this._options.length - 1; - } + var option = { + shortName: shortName, + longName: longName, + hasValue: hasValue, + helpText: helpText, + canHaveMultiple: (canHaveMultiple || false), + coercer: coercer + }; + + this._addOption(option); +}; + +// TODO: refactor addOption to accept objects, then get rid of this method +/** + * Provide information about an option that should not cause an error if present, but that is always + * ignored (for example, an option that was used in previous versions but is no longer supported). + * + * @private + * @param {string} shortName - The short name of the option with a leading hyphen (for example, + * `-v`). + * @param {string} longName - The long name of the option with two leading hyphens (for example, + * `--version`). + */ +ArgParser.prototype.addIgnoredOption = function(shortName, longName) { + var option = { + shortName: shortName, + longName: longName, + ignore: true + }; + + this._addOption(option); }; function padding(length) { - return new Array(length + 1).join(' '); + return new Array(length + 1).join(' '); } function padLeft(str, length) { - return padding(length) + str; + return padding(length) + str; } function padRight(str, length) { - return str + padding(length); + return str + padding(length); } function findMaxLength(arr) { - var max = 0; + var max = 0; - arr.forEach(function(item) { - if (item.length > max) { - max = item.length; - } - }); + arr.forEach(function(item) { + if (item.length > max) { + max = item.length; + } + }); - return max; + return max; } function concatWithMaxLength(items, maxLength) { - var result = ''; - // to prevent endless loops, always use the first item, regardless of length - result += items.shift(); + var result = ''; + // to prevent endless loops, always use the first item, regardless of length + result += items.shift(); - while ( items.length && (result.length + items[0].length < maxLength) ) { - result += ' ' + items.shift(); - } + while ( items.length && (result.length + items[0].length < maxLength) ) { + result += ' ' + items.shift(); + } - return result; + return result; } // we want to format names and descriptions like this: // | -f, --foo Very long description very long description very long | // | description very long description. | function formatHelpInfo(options) { - var MARGIN_LENGTH = 4; - var results = []; + var MARGIN_LENGTH = 4; + var results = []; - var maxLength = process.stdout.columns; - var maxNameLength = findMaxLength(options.names); - var maxDescriptionLength = findMaxLength(options.descriptions); + var maxLength = process.stdout.columns; + var maxNameLength = findMaxLength(options.names); + var maxDescriptionLength = findMaxLength(options.descriptions); - var wrapDescriptionAt = maxLength - (MARGIN_LENGTH * 3) - maxNameLength; - // build the string for each option - options.names.forEach(function(name, i) { - var result; - var partialDescription; - var words; + var wrapDescriptionAt = maxLength - (MARGIN_LENGTH * 3) - maxNameLength; + // build the string for each option + options.names.forEach(function(name, i) { + var result; + var partialDescription; + var words; - // add a left margin to the name - result = padLeft(options.names[i], MARGIN_LENGTH); - // and a right margin, with extra padding so the descriptions line up with one another - result = padRight(result, maxNameLength - options.names[i].length + MARGIN_LENGTH); + // add a left margin to the name + result = padLeft(options.names[i], MARGIN_LENGTH); + // and a right margin, with extra padding so the descriptions line up with one another + result = padRight(result, maxNameLength - options.names[i].length + MARGIN_LENGTH); - // split the description on spaces - words = options.descriptions[i].split(' '); - // add as much of the description as we can fit on the first line - result += concatWithMaxLength(words, wrapDescriptionAt); - // if there's anything left, keep going until we've consumed the description - while (words.length) { - partialDescription = padding( maxNameLength + (MARGIN_LENGTH * 2) ); - partialDescription += concatWithMaxLength(words, wrapDescriptionAt); - result += '\n' + partialDescription; - } + // split the description on spaces + words = options.descriptions[i].split(' '); + // add as much of the description as we can fit on the first line + result += concatWithMaxLength(words, wrapDescriptionAt); + // if there's anything left, keep going until we've consumed the description + while (words.length) { + partialDescription = padding( maxNameLength + (MARGIN_LENGTH * 2) ); + partialDescription += concatWithMaxLength(words, wrapDescriptionAt); + result += '\n' + partialDescription; + } - results.push(result); - }); + results.push(result); + }); - return results; + return results; } /** - Generate a summary of all the options with corresponding help text. - @returns {string} + * Generate a summary of all the options with corresponding help text. + * @returns {string} */ ArgParser.prototype.help = function() { - var options = { - names: [], - descriptions: [] - }; + var options = { + names: [], + descriptions: [] + }; - this._options.forEach(function(option) { - var name = ''; + this._options.forEach(function(option) { + var name = ''; - if (option.shortName) { - name += '-' + option.shortName + (option.longName ? ', ' : ''); - } + // don't show ignored options + if (option.ignore) { + return; + } - if (option.longName) { - name += '--' + option.longName; - } + if (option.shortName) { + name += '-' + option.shortName + (option.longName ? ', ' : ''); + } - if (option.hasValue) { - name += ' '; - } + if (option.longName) { + name += '--' + option.longName; + } - options.names.push(name); - options.descriptions.push(option.helpText); - }); + if (option.hasValue) { + name += ' '; + } - return 'Options:\n' + formatHelpInfo(options).join('\n'); + options.names.push(name); + options.descriptions.push(option.helpText); + }); + + return 'Options:\n' + formatHelpInfo(options).join('\n'); }; /** - Get the options. - @param {Array.} args An array, like ['-x', 'hello'] - @param {Object} [defaults={}] An optional collection of default values. - @returns {Object} The keys will be the longNames, or the shortName if - no longName is defined for that option. The values will be the values - provided, or `true` if the option accepts no value. + * Get the options. + * @param {Array.} args An array, like ['-x', 'hello'] + * @param {Object} [defaults={}] An optional collection of default values. + * @returns {Object} The keys will be the longNames, or the shortName if no longName is defined for + * that option. The values will be the values provided, or `true` if the option accepts no value. */ ArgParser.prototype.parse = function(args, defaults) { - var result = defaults && _.defaults({}, defaults) || {}; + var result = defaults && _.defaults({}, defaults) || {}; - result._ = []; - for (var i = 0, leni = args.length; i < leni; i++) { - var arg = '' + args[i], - next = (i < leni-1)? '' + args[i+1] : null, - option, - shortName = null, - longName, - name, - value = null; + result._ = []; + for (var i = 0, leni = args.length; i < leni; i++) { + var arg = '' + args[i], + next = (i < leni-1)? '' + args[i+1] : null, + option, + shortName = null, + longName, + name, + value = null; - // like -t - if (arg.charAt(0) === '-') { + // like -t + if (arg.charAt(0) === '-') { - // like --template - if (arg.charAt(1) === '-') { - name = longName = arg.slice(2); - option = this._getOptionByLongName(longName); - } - else { - name = shortName = arg.slice(1); - option = this._getOptionByShortName(shortName); - } + // like --template + if (arg.charAt(1) === '-') { + name = longName = arg.slice(2); + option = this._getOptionByLongName(longName); + } + else { + name = shortName = arg.slice(1); + option = this._getOptionByShortName(shortName); + } - if (option === null) { - throw new Error( 'Unknown command line option found: ' + name ); - } + if (option === null) { + throw new Error( 'Unknown command line option found: ' + name ); + } - if (option.hasValue) { - value = next; - i++; + if (option.hasValue) { + value = next; + i++; - if (value === null || value.charAt(0) === '-') { - throw new Error( 'Command line option requires a value: ' + name ); - } - } - else { - value = true; - } + if (value === null || value.charAt(0) === '-') { + throw new Error( 'Command line option requires a value: ' + name ); + } + } + else { + value = true; + } - if (option.longName && shortName) { - name = option.longName; - } + // skip ignored options now that we've consumed the option text + if (option.ignore) { + continue; + } - if (typeof option.coercer === 'function') { - value = option.coercer(value); - } - - // Allow for multiple options of the same type to be present - if (option.canHaveMultiple && hasOwnProp.call(result, name)) { - var val = result[name]; - - if (val instanceof Array) { - val.push(value); - } else { - result[name] = [val, value]; - } - } - else { - result[name] = value; - } - } - else { - result._.push(arg); - } - } + if (option.longName && shortName) { + name = option.longName; + } - return result; + if (typeof option.coercer === 'function') { + value = option.coercer(value); + } + + // Allow for multiple options of the same type to be present + if (option.canHaveMultiple && hasOwnProp.call(result, name)) { + var val = result[name]; + + if (val instanceof Array) { + val.push(value); + } else { + result[name] = [val, value]; + } + } + else { + result[name] = value; + } + } + else { + result._.push(arg); + } + } + + return result; }; module.exports = ArgParser; diff --git a/lib/jsdoc/opts/args.js b/lib/jsdoc/opts/args.js index 65e06715..ba8e3896 100644 --- a/lib/jsdoc/opts/args.js +++ b/lib/jsdoc/opts/args.js @@ -1,67 +1,76 @@ /** - @module jsdoc/opts/args - @requires jsdoc/opts/argparser - @author Michael Mathews - @license Apache License 2.0 - See file 'LICENSE.md' in this project. + * @module jsdoc/opts/args + * @requires jsdoc/opts/argparser + * @author Michael Mathews + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. */ 'use strict'; -var ArgParser = require('jsdoc/opts/argparser'), - argParser = new ArgParser(), - hasOwnProp = Object.prototype.hasOwnProperty, - ourOptions, - querystring = require('querystring'), - util = require('util'); +var ArgParser = require('jsdoc/opts/argparser'); +var querystring = require('querystring'); +var util = require('util'); +var ourOptions; + +var argParser = new ArgParser(); +var hasOwnProp = Object.prototype.hasOwnProperty; // cast strings to booleans or integers where appropriate function castTypes(item) { - var result = item; + var integer; - switch (result) { - case 'true': - return true; - case 'false': - return false; - default: - // might be an integer - var integer = parseInt(result, 10); - if (String(integer) === result && integer !== 'NaN') { - return integer; - } else { - return result; - } - } + var result = item; + + switch (result) { + case 'true': + result = true; + break; + + case 'false': + result = false; + break; + + default: + // might be an integer + integer = parseInt(result, 10); + if (String(integer) === result && integer !== 'NaN') { + result = integer; + } + } + + return result; } // check for strings that we need to cast to other types function fixTypes(item) { - var result = item; + var result = item; - // recursively process arrays and objects - if ( util.isArray(result) ) { - for (var i = 0, l = result.length; i < l; i++) { - result[i] = fixTypes(result[i]); - } - } else if (typeof result === 'object') { - Object.keys(result).forEach(function(prop) { - result[prop] = fixTypes(result[prop]); - }); - } else { - result = castTypes(result); - } + // recursively process arrays and objects + if ( util.isArray(result) ) { + for (var i = 0, l = result.length; i < l; i++) { + result[i] = fixTypes(result[i]); + } + } + else if (typeof result === 'object') { + Object.keys(result).forEach(function(prop) { + result[prop] = fixTypes(result[prop]); + }); + } + else { + result = castTypes(result); + } - return result; + return result; } function parseQuery(str) { - var result = querystring.parse(str); + var result = querystring.parse(str); - Object.keys(result).forEach(function(prop) { - result[prop] = fixTypes(result[prop]); - }); + Object.keys(result).forEach(function(prop) { + result[prop] = fixTypes(result[prop]); + }); - return result; + return result; } argParser.addOption('t', 'template', true, 'The path to the template to use. Default: path/to/jsdoc/templates/default'); @@ -71,58 +80,59 @@ argParser.addOption('T', 'test', false, 'Run all tests and quit.'); argParser.addOption('d', 'destination', true, 'The path to the output folder. Use "console" to dump data to the console. Default: ./out/'); argParser.addOption('p', 'private', false, 'Display symbols marked with the @private tag. Default: false'); argParser.addOption('r', 'recurse', false, 'Recurse into subdirectories when scanning for source code files.'); -argParser.addOption('l', 'lenient', false, 'Continue to generate output if a doclet is incomplete or contains errors. Default: false'); argParser.addOption('h', 'help', false, 'Print this message and quit.'); argParser.addOption('X', 'explain', false, 'Dump all found doclet internals to console and quit.'); argParser.addOption('q', 'query', true, 'A query string to parse and store in env.opts.query. Example: foo=bar&baz=true', false, parseQuery); argParser.addOption('u', 'tutorials', true, 'Directory in which JSDoc should search for tutorials.'); argParser.addOption('v', 'version', false, 'Display the version number and quit.'); +argParser.addOption('', 'debug', false, 'Log information for debugging JSDoc. On Rhino, launches the debugger when passed as the first option.'); +argParser.addOption('', 'verbose', false, 'Log detailed information to the console as JSDoc runs.'); +argParser.addOption('', 'pedantic', false, 'Treat errors as fatal errors, and treat warnings as errors. Default: false'); -//TODO [-R, recurseonly] = a number representing the depth to recurse -//TODO [-f, filter] = a regex to filter on <-- this can be better defined in the configs? +// Options specific to tests +argParser.addOption(null, 'match', true, 'Only run tests containing .', true); +argParser.addOption(null, 'nocolor', false, 'Do not use color in console output from tests.'); -//Here are options specific to tests -argParser.addOption(null, 'verbose', false, 'Display verbose output for tests'); -argParser.addOption(null, 'match', true, 'Only run tests containing ', true); -argParser.addOption(null, 'nocolor', false, 'Do not use color in console output from tests'); +// Options that are no longer supported and should be ignored +argParser.addIgnoredOption('l', 'lenient'); // removed in JSDoc 3.3.0 /** - Set the options for this app. - @throws {Error} Illegal arguments will throw errors. - @param {string|String[]} args The command line arguments for this app. + * Set the options for this app. + * @throws {Error} Illegal arguments will throw errors. + * @param {string|String[]} args The command line arguments for this app. */ exports.parse = function(args) { - args = args || []; + args = args || []; - if (typeof args === 'string' || args.constructor === String) { - args = (''+args).split(/\s+/g); - } + if (typeof args === 'string' || args.constructor === String) { + args = (''+args).split(/\s+/g); + } - ourOptions = argParser.parse(args); + ourOptions = argParser.parse(args); - return ourOptions; + return ourOptions; }; /** - Display help message for options. + * Retrieve help message for options. */ exports.help = function() { return argParser.help(); }; /** - Get a named option. - @param {string} name The name of the option. - @return {string} The value associated with the given name. + * Get a named option. + * @param {string} name The name of the option. + * @return {string} The value associated with the given name. *//** - Get all the options for this app. - @return {Object} A collection of key/values representing all the options. + * Get all the options for this app. + * @return {Object} A collection of key/values representing all the options. */ exports.get = function(name) { - if (typeof name === 'undefined') { - return ourOptions; - } - else { - return ourOptions[name]; - } + if (typeof name === 'undefined') { + return ourOptions; + } + else if ( hasOwnProp.call(ourOptions, name) ) { + return ourOptions[name]; + } }; diff --git a/lib/jsdoc/plugins.js b/lib/jsdoc/plugins.js index 8f9e6ead..a9aa696b 100644 --- a/lib/jsdoc/plugins.js +++ b/lib/jsdoc/plugins.js @@ -5,7 +5,7 @@ */ 'use strict'; -var error = require('jsdoc/util/error'); +var logger = require('jsdoc/util/logger'); var path = require('jsdoc/path'); exports.installPlugins = function(plugins, parser) { @@ -19,8 +19,7 @@ exports.installPlugins = function(plugins, parser) { pluginPath = path.getResourcePath(path.dirname(plugins[i]), path.basename(plugins[i])); if (!pluginPath) { - error.handle(new Error('Unable to find the plugin "' + plugins[i] + '"')); - continue; + logger.error('Unable to find the plugin "%s"', plugins[i]); } plugin = require(pluginPath); @@ -41,8 +40,8 @@ exports.installPlugins = function(plugins, parser) { //...add a Rhino node visitor (deprecated in JSDoc 3.3) if (plugin.nodeVisitor) { if ( !parser.addNodeVisitor ) { - error.handle( new Error('Unable to add the Rhino node visitor from ' + plugins[i] + - ', because JSDoc is not using the Rhino JavaScript parser.') ); + logger.error('Unable to add the Rhino node visitor from %s, because JSDoc ' + + 'is not using the Rhino JavaScript parser.', plugins[i]); } else { parser.addNodeVisitor(plugin.nodeVisitor); diff --git a/lib/jsdoc/src/astnode.js b/lib/jsdoc/src/astnode.js index 44234795..02bfb8bb 100644 --- a/lib/jsdoc/src/astnode.js +++ b/lib/jsdoc/src/astnode.js @@ -143,6 +143,7 @@ var nodeToString = exports.nodeToString = function(node) { str = node.operator + str; } else { + // this shouldn't happen throw new Error('Found a UnaryExpression with a postfix operator: %j', node); } break; diff --git a/lib/jsdoc/src/handlers.js b/lib/jsdoc/src/handlers.js index 028d93f1..66218e1e 100644 --- a/lib/jsdoc/src/handlers.js +++ b/lib/jsdoc/src/handlers.js @@ -18,7 +18,7 @@ function getNewDoclet(comment, e) { catch (error) { err = new Error( util.format('cannot create a doclet for the comment "%s": %s', comment.replace(/[\r\n]/g, ''), error.message) ); - require('jsdoc/util/error').handle(err); + require('jsdoc/util/logger').error(err); doclet = new Doclet('', e); } diff --git a/lib/jsdoc/src/parser.js b/lib/jsdoc/src/parser.js index d3823e91..faee26ef 100644 --- a/lib/jsdoc/src/parser.js +++ b/lib/jsdoc/src/parser.js @@ -12,6 +12,7 @@ var jsdoc = { syntax: require('jsdoc/src/syntax') } }; +var logger = require('jsdoc/util/logger'); var util = require('util'); var hasOwnProp = Object.prototype.hasOwnProperty; @@ -63,7 +64,7 @@ exports.createParser = function(type) { return new ( require(modulePath) ).Parser(); } catch (e) { - throw new Error('Unable to create the parser type "' + type + '": ' + e); + logger.fatal('Unable to create the parser type "' + type + '": ' + e); } }; @@ -142,6 +143,7 @@ Parser.prototype.parse = function(sourceFiles, encoding) { } e.sourcefiles = sourceFiles; + logger.debug('Parsing source files: %j', sourceFiles); this.emit('parseBegin', e); @@ -158,10 +160,7 @@ Parser.prototype.parse = function(sourceFiles, encoding) { sourceCode = require('jsdoc/fs').readFileSync(filename, encoding); } catch(e) { - // TODO: shouldn't this be fatal if we're not in lenient mode? - console.error('FILE READ ERROR: in module:jsdoc/src/parser.Parser#parse: "' + - filename + '" ' + e); - continue; + logger.error('Unable to read and parse the source file %s: %s', filename, e); } } @@ -175,6 +174,7 @@ Parser.prototype.parse = function(sourceFiles, encoding) { sourcefiles: parsedFiles, doclets: this._resultBuffer }); + logger.debug('Finished parsing source files.'); return this._resultBuffer; }; @@ -235,6 +235,7 @@ Parser.prototype._parseSourceCode = function(sourceCode, sourceName) { }; this.emit('fileBegin', e); + logger.printInfo('Parsing %s ...', sourceName); if (!e.defaultPrevented) { e = { @@ -252,6 +253,7 @@ Parser.prototype._parseSourceCode = function(sourceCode, sourceName) { } this.emit('fileComplete', e); + logger.info('complete.'); }; // TODO: docs diff --git a/lib/jsdoc/src/scanner.js b/lib/jsdoc/src/scanner.js index 91b7ecb9..d3ae9482 100644 --- a/lib/jsdoc/src/scanner.js +++ b/lib/jsdoc/src/scanner.js @@ -9,6 +9,7 @@ 'use strict'; var fs = require('jsdoc/fs'); +var logger = require('jsdoc/util/logger'); var path = require('jsdoc/path'); /** @@ -25,6 +26,7 @@ exports.Scanner.prototype = Object.create( require('events').EventEmitter.protot @fires sourceFileFound */ exports.Scanner.prototype.scan = function(searchPaths, depth, filter) { + var currentFile; var isFile; var filePaths = []; @@ -38,13 +40,14 @@ exports.Scanner.prototype.scan = function(searchPaths, depth, filter) { var filepath = path.resolve( pwd, decodeURIComponent($) ); try { - isFile = fs.statSync(filepath).isFile(); + currentFile = fs.statSync(filepath); } - catch(e) { - isFile = false; + catch (e) { + logger.error('Unable to find the source file or directory %s', filepath); + return; } - if (isFile) { + if ( currentFile.isFile() ) { filePaths.push(filepath); } else { diff --git a/lib/jsdoc/tag.js b/lib/jsdoc/tag.js index 894dc5b4..d6985cf4 100644 --- a/lib/jsdoc/tag.js +++ b/lib/jsdoc/tag.js @@ -19,8 +19,12 @@ var jsdoc = { dictionary: require('jsdoc/tag/dictionary'), validator: require('jsdoc/tag/validator'), type: require('jsdoc/tag/type') + }, + util: { + logger: require('jsdoc/util/logger') } }; +var path = require('jsdoc/path'); function trim(text, opts) { var indentMatcher; @@ -117,8 +121,19 @@ var Tag = exports.Tag = function(tagTitle, tagBody, meta) { this.text = trim(tagBody, trimOpts); if (this.text) { - processTagText(this, tagDef); + try { + processTagText(this, tagDef); + jsdoc.tag.validator.validate(this, tagDef, meta); + } + catch (e) { + // probably a type-parsing error + jsdoc.util.logger.error( + 'Unable to create a Tag object%s with title "%s" and body "%s": %s', + meta.filename ? ( ' for source file ' + path.join(meta.path, meta.filename) ) : '', + tagTitle, + tagBody, + e.message + ); + } } - - jsdoc.tag.validator.validate(this, tagDef, meta); }; diff --git a/lib/jsdoc/tag/type.js b/lib/jsdoc/tag/type.js index 35fdd509..b25953aa 100644 --- a/lib/jsdoc/tag/type.js +++ b/lib/jsdoc/tag/type.js @@ -239,8 +239,8 @@ function parseTypeExpression(tagInfo) { } catch (e) { // always re-throw so the caller has a chance to report which file was bad - throw new Error( util.format('unable to parse the type expression "%s": %s', - tagInfo.typeExpression, e.message) ); + throw new Error( util.format('Invalid type expression "%s": %s', tagInfo.typeExpression, + e.message) ); } if (parsedType) { diff --git a/lib/jsdoc/tag/validator.js b/lib/jsdoc/tag/validator.js index a58b2e9f..6ba3b1d7 100644 --- a/lib/jsdoc/tag/validator.js +++ b/lib/jsdoc/tag/validator.js @@ -10,6 +10,7 @@ var dictionary = require('jsdoc/tag/dictionary'); var format = require('util').format; +var logger = require('jsdoc/util/logger'); function buildMessage(tagName, meta, desc) { var result = format('The @%s tag %s. File: %s, line: %s', tagName, desc, meta.filename, @@ -20,43 +21,21 @@ function buildMessage(tagName, meta, desc) { return result; } -function UnknownTagError(tagName, meta) { - this.name = 'UnknownTagError'; - this.message = buildMessage(tagName, meta, 'is not a known tag'); -} -UnknownTagError.prototype = new Error(); -UnknownTagError.prototype.constructor = UnknownTagError; - -function TagValueRequiredError(tagName, meta) { - this.name = 'TagValueRequiredError'; - this.message = buildMessage(tagName, meta, 'requires a value'); -} -TagValueRequiredError.prototype = new Error(); -TagValueRequiredError.prototype.constructor = TagValueRequiredError; - -function TagValueNotPermittedError(tagName, meta) { - this.name = 'TagValueNotPermittedError'; - this.message = buildMessage(tagName, meta, 'does not permit a value'); -} -TagValueNotPermittedError.prototype = new Error(); -TagValueNotPermittedError.prototype.constructor = TagValueNotPermittedError; - /** * Validate the given tag. */ exports.validate = function(tag, tagDef, meta) { if (!tagDef && !env.conf.tags.allowUnknownTags) { - require('jsdoc/util/error').handle( new UnknownTagError(tag.title, meta) ); + logger.error( buildMessage(tag.title, meta, 'is not a known tag') ); } - - if (!tag.text) { + else if (!tag.text) { if (tagDef.mustHaveValue) { - require('jsdoc/util/error').handle( new TagValueRequiredError(tag.title, meta) ); + logger.error( buildMessage(tag.title, meta, 'requires a value') ); } } else { if (tagDef.mustNotHaveValue) { - require('jsdoc/util/error').handle( new TagValueNotPermittedError(tag.title, meta) ); + logger.error( buildMessage(tag.title, meta, 'does not permit a value') ); } } }; diff --git a/lib/jsdoc/tutorial/resolver.js b/lib/jsdoc/tutorial/resolver.js index daf8b924..c37b8809 100644 --- a/lib/jsdoc/tutorial/resolver.js +++ b/lib/jsdoc/tutorial/resolver.js @@ -12,7 +12,7 @@ var tutorial = require('jsdoc/tutorial'), fs = require('jsdoc/fs'), - error = require('jsdoc/util/error'), + logger = require('jsdoc/util/logger'), path = require('path'), hasOwnProp = Object.prototype.hasOwnProperty, conf = {}, @@ -61,7 +61,7 @@ function addTutorialConf(name, meta) { } // check if the tutorial has already been defined... if (hasOwnProp.call(conf, name)) { - error.handle(new Error("Tutorial " + name + "'s metadata is defined multiple times, only the first will be used.")); + logger.warn('Metadata for the tutorial %s is defined more than once. Only the first definition will be used.', name ); } else { conf[name] = meta; } @@ -79,7 +79,7 @@ function addTutorialConf(name, meta) { */ exports.addTutorial = function(current) { if (hasOwnProp.call(tutorials, current.name)) { - error.handle(new Error("Tutorial with name " + current.name + " exists more than once, not adding (same name, different file extensions?)")); + logger.warn('The tutorial %s is defined more than once. Only the first definition will be used.', current.name); } else { tutorials[current.name] = current; @@ -179,7 +179,7 @@ exports.resolve = function() { if (item.children) { item.children.forEach(function(child) { if (!hasOwnProp.call(tutorials, child)) { - error.handle( new Error("Missing child tutorial: " + child) ); + logger.error('Missing child tutorial: %s', child); } else { tutorials[child].setParent(current); diff --git a/lib/jsdoc/util/error.js b/lib/jsdoc/util/error.js index 406c9166..6ed1895d 100644 --- a/lib/jsdoc/util/error.js +++ b/lib/jsdoc/util/error.js @@ -1,33 +1,35 @@ /*global env: true */ /** - Helper functions for handling errors. - @module jsdoc/util/error + * Helper functions for handling errors. + * + * @deprecated As of JSDoc 3.3.0. This module may be removed in a future release. Use the module + * {@link module:jsdoc/util/logger} to log warnings and errors. + * @module jsdoc/util/error */ 'use strict'; /** - Handle an exception appropriately based on whether lenient mode is enabled: - - + If lenient mode is enabled, log the exception to the console. - + If lenient mode is not enabled, re-throw the exception. - @param {Error} e - The exception to handle. - @exception {Error} Re-throws the original exception unless lenient mode is enabled. - @memberof module:jsdoc/util/error + * Log an exception as an error. + * + * Prior to JSDoc 3.3.0, this method would either log the exception (if lenient mode was enabled) or + * re-throw the exception (default). + * + * In JSDoc 3.3.0 and later, lenient mode has been replaced with strict mode, which is disabled by + * default. If strict mode is enabled, calling the `handle` method causes JSDoc to exit immediately, + * just as if the exception had been re-thrown. + * + * @deprecated As of JSDoc 3.3.0. This module may be removed in a future release. + * @param {Error} e - The exception to log. + * @memberof module:jsdoc/util/error */ exports.handle = function(e) { - var msg; + var logger = require('jsdoc/util/logger'); + var msg = e ? ( e.message || JSON.stringify(e) ) : ''; - if (env.opts.lenient) { - msg = e.message || JSON.stringify(e); + // include the error type if it's an Error object + if (e instanceof Error) { + msg = e.name + ': ' + msg; + } - // include the error type if it's an Error object - if (e instanceof Error) { - msg = e.name + ': ' + msg; - } - - console.log(msg); - } - else { - throw e; - } + logger.error(msg); }; diff --git a/lib/jsdoc/util/logger.js b/lib/jsdoc/util/logger.js new file mode 100644 index 00000000..df644fa5 --- /dev/null +++ b/lib/jsdoc/util/logger.js @@ -0,0 +1,232 @@ +/** + * Logging tools for JSDoc. + * + * Log messages are printed to the console based on the current logging level. By default, messages + * at level `{@link module:jsdoc/util/logger.LEVELS.ERROR}` or above are logged; all other messages + * are ignored. + * + * In addition, the module object emits an event whenever a logger method is called, regardless of + * the current logging level. The event's name is the string `logger:` followed by the logger's name + * (for example, `logger:error`). The event handler receives an array of arguments that were passed + * to the logger method. + * + * Each logger method accepts a `message` parameter that may contain zero or more placeholders. Each + * placeholder is replaced by the corresponding argument following the message. If the placeholder + * does not have a corresponding argument, the placeholder is not replaced. + * + * The following placeholders are supported: + * + * + `%s`: String. + * + `%d`: Number. + * + `%j`: JSON. + * + * @module jsdoc/util/logger + * @extends module:events.EventEmitter + * @example + * var logger = require('jsdoc/util/logger'); + * + * var data = { + * foo: 'bar' + * }; + * var name = 'baz'; + * + * logger.warn('%j %s', data, name); // prints '{"foo":"bar"} baz' + * @see http://nodejs.org/api/util.html#util_util_format_format + */ +'use strict'; + +var runtime = require('jsdoc/util/runtime'); +var util = require('util'); + +function Logger() {} +util.inherits(Logger, require('events').EventEmitter); + +var logger = module.exports = new Logger(); + +/** + * Logging levels for the JSDoc logger. The default logging level is + * {@link module:jsdoc/util/logger.LEVELS.ERROR}. + * + * @enum + * @type {number} + */ +var LEVELS = logger.LEVELS = { + /** Do not log any messages. */ + SILENT: 0, + /** Log fatal errors that prevent JSDoc from running. */ + FATAL: 10, + /** Log all errors, including errors from which JSDoc can recover. */ + ERROR: 20, + /** + * Log the following messages: + * + * + Warnings + * + Errors + */ + WARN: 30, + /** + * Log the following messages: + * + * + Informational messages + * + Warnings + * + Errors + */ + INFO: 40, + /** + * Log the following messages: + * + * + Debugging messages + * + Informational messages + * + Warnings + * + Errors + */ + DEBUG: 50, + /** Log all messages. */ + VERBOSE: 1000 +}; + +var DEFAULT_LEVEL = LEVELS.ERROR; +var logLevel = DEFAULT_LEVEL; + +var PREFIXES = { + DEBUG: 'DEBUG: ', + ERROR: 'ERROR: ', + FATAL: 'FATAL: ', + WARN: 'WARNING: ' +}; + +// Add a prefix to a log message if necessary. +function addPrefix(args, prefix) { + var updatedArgs; + + if (prefix && typeof args[0] === 'string') { + updatedArgs = args.slice(0); + updatedArgs[0] = prefix + updatedArgs[0]; + } + + return updatedArgs || args; +} + +// TODO: document events +function wrapLogFunction(name, func) { + var eventName = 'logger:' + name; + var upperCaseName = name.toUpperCase(); + var level = LEVELS[upperCaseName]; + var prefix = PREFIXES[upperCaseName]; + + return function() { + var loggerArgs; + + var args = Array.prototype.slice.call(arguments, 0); + + if (logLevel >= level) { + loggerArgs = addPrefix(args, prefix); + func.apply(null, loggerArgs); + } + + args.unshift(eventName); + logger.emit.apply(logger, args); + }; +} + +// Print a message to STDOUT without a terminating newline. +function printToStdout() { + var args = Array.prototype.slice.call(arguments, 0); + + util.print( util.format.apply(util, args) ); +} + +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.debug = wrapLogFunction('debug', console.info); +/** + * Print a string at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}. The string is not + * terminated by a newline. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.printDebug = wrapLogFunction('debug', printToStdout); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.ERROR}. + * + * @name module:jsdoc/util/logger.error + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.error = wrapLogFunction('error', console.error); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.FATAL}. + * + * @name module:jsdoc/util/logger.error + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.fatal = wrapLogFunction('fatal', console.error); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.INFO}. + * + * @name module:jsdoc/util/logger.info + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.info = wrapLogFunction('info', console.info); +/** + * Print a string at log level {@link module:jsdoc/util/logger.LEVELS.INFO}. The string is not + * terminated by a newline. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.printInfo = wrapLogFunction('info', printToStdout); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}. + * + * @name module:jsdoc/util/logger.verbose + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.verbose = wrapLogFunction('verbose', console.info); +/** + * Print a string at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}. The string is not + * terminated by a newline. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.printVerbose = wrapLogFunction('verbose', printToStdout); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.WARN}. + * + * @name module:jsdoc/util/logger.warn + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.warn = wrapLogFunction('warn', console.warn); + +/** + * Set the log level. + * + * @param {module:jsdoc/util/logger.LEVELS} level - The log level to use. + */ +logger.setLevel = function setLevel(level) { + logLevel = (level !== undefined) ? level : DEFAULT_LEVEL; +}; + +/** + * Get the current log level. + * + * @return {module:jsdoc/util/logger.LEVELS} The current log level. + */ +logger.getLevel = function getLevel() { + return logLevel; +}; diff --git a/lib/jsdoc/util/markdown.js b/lib/jsdoc/util/markdown.js index c19e7fcc..0ec0d094 100644 --- a/lib/jsdoc/util/markdown.js +++ b/lib/jsdoc/util/markdown.js @@ -73,9 +73,9 @@ function unescapeUrls(source) { * @param {Object} [conf] Configuration for the selected parser, if any. * @returns {Function} A function that accepts Markdown source, feeds it to the selected parser, and * returns the resulting HTML. - * @throws {Error} If the name does not correspond to a known parser. */ function getParseFunction(parserName, conf) { + var logger = require('jsdoc/util/logger'); var marked = require('marked'); var parserFunction; @@ -99,7 +99,8 @@ function getParseFunction(parserName, conf) { return parserFunction; } else { - throw new Error("unknown Markdown parser: '" + parserName + "'"); + logger.error('Unrecognized Markdown parser "%s". Markdown support is disabled.', + parserName); } } @@ -108,9 +109,9 @@ function getParseFunction(parserName, conf) { * `env.conf.markdown` property. The parsing function accepts a single parameter containing Markdown * source. The function uses the parser specified in `conf.json` to transform the Markdown source to * HTML, then returns the HTML as a string. - * @returns {Function} A function that accepts Markdown source, feeds it to the selected parser, and + * + * @returns {function} A function that accepts Markdown source, feeds it to the selected parser, and * returns the resulting HTML. - * @throws {Error} If the value of `env.conf.markdown.parser` does not correspond to a known parser. */ exports.getParser = function() { var conf = env.conf.markdown; diff --git a/lib/jsdoc/util/runtime.js b/lib/jsdoc/util/runtime.js index 678d67ab..4b226625 100644 --- a/lib/jsdoc/util/runtime.js +++ b/lib/jsdoc/util/runtime.js @@ -107,7 +107,7 @@ exports.initialize = function(args) { initializeNode(args); break; default: - throw new Error('Unable to initialize the JavaScript runtime!'); + throw new Error('Cannot initialize the unknown JavaScript runtime "' + runtime + '"!'); } }; diff --git a/lib/jsdoc/util/templateHelper.js b/lib/jsdoc/util/templateHelper.js index 75891acb..4712fd05 100644 --- a/lib/jsdoc/util/templateHelper.js +++ b/lib/jsdoc/util/templateHelper.js @@ -124,7 +124,7 @@ function parseType(longname) { } catch (e) { err = new Error('unable to parse ' + longname + ': ' + e.message); - require('jsdoc/util/error').handle(err); + require('jsdoc/util/logger').error(err); return longname; } } @@ -285,7 +285,7 @@ var tutorialToUrl = exports.tutorialToUrl = function(tutorial) { var node = tutorials.getByName(tutorial); // no such tutorial if (!node) { - require('jsdoc/util/error').handle( new Error('No such tutorial: '+tutorial) ); + require('jsdoc/util/logger').error( new Error('No such tutorial: ' + tutorial) ); return; } @@ -317,7 +317,7 @@ var tutorialToUrl = exports.tutorialToUrl = function(tutorial) { */ var toTutorial = exports.toTutorial = function(tutorial, content, missingOpts) { if (!tutorial) { - require('jsdoc/util/error').handle( new Error('Missing required parameter: tutorial') ); + require('jsdoc/util/logger').error( new Error('Missing required parameter: tutorial') ); return; } diff --git a/package.json b/package.json index 365265f6..6e0a9dd2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jsdoc", "version": "3.3.0-dev", - "revision": "1386539957698", + "revision": "1387841089759", "description": "An API documentation generator for JavaScript.", "keywords": [ "documentation", "javascript" ], "licenses": [ diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 04b6c681..00000000 --- a/plugins/README.md +++ /dev/null @@ -1,389 +0,0 @@ -Creating and Enabling a Plugin ----- - -There are two steps required to create and enable a new JSDoc plugin: - -1. Create a JavaScript module to contain your plugin code. -2. Include that module in the "plugins" array of `conf.json`. You can specify -an absolute or relative path. If you use a relative path, JSDoc searches for -the plugin in the current working directory and the JSDoc directory, in that -order. - -For example, if your plugin source code was saved in the "plugins/shout.js" -file in the current working directory, you would include it by adding a -reference to it in conf.json like so: - - ... - "plugins": [ - "plugins/shout" - ] - ... - -Authoring JSDoc 3 Plugins ----- - -The plugin system for JSDoc 3 is pretty powerful and provides plugin authors -multiple methods, from high-level to low-level, of affecting document generation: - -- Defining event handlers -- Defining tags -- Defining a parse tree node processor - -### Event Handlers - -At the highest level, a plugin may register handlers for specific named-events -that occur in the documentation generation process. JSDoc will pass the handler -an event object containing pertinent information. Your plugin module should -export a _handlers_ object that contains your handler, like so: - - exports.handlers = { - newDoclet: function(e) { - //Do something when we see a new doclet - } - } - -#### Event: fileBegin - -This is triggered when the parser has started on a new file. You might use this -to do any per-file initialization your plugin needs to do. - -The event object will contain the following properties: - -- filename: the name of the file - -#### Event: beforeParse - -This is triggered before parsing has begun. You can use this method to modify -the source code that will be parsed. For instance, you might add some virtual -doclets so they get added to the documentation. - -The event object will contain the following properties: - -- filename: the name of the file -- source: the contents of the file - -Below is an example that adds a virtual doclet for a function to the source so -that it will get parsed and added to the documentation. This might be done to -document methods that will be present for end-user use, but might not be in the -source code being documented, like methods provided by a third-party superclass: - - exports.handlers = { - beforeParse: function(e) { - var extraDoc = ["", - "/**", - "Here's a description of this function", - "@name superFunc", - "@memberof ui.mywidget", - "@function", - "*/", ""]; - e.source += extraDoc.join("\n"); - } - } - -#### Event: jsdocCommentFound - -This is fired whenever a jsdoc comment is found. It may or may not be associated -with any code. You might use this to modify the contents of a comment before it -is processed. - -The event object will contain the following properties: - -- filename: the name of the file -- comment: the text of the comment -- lineno: the line number the comment was found on - -#### Event: symbolFound - -This is fired when the parser comes across a symbol in the code it thinks is -important. This usually means things that one might want to document -- -variables, functions, object literals, object property definitions, -assignments, etc., but the symbols the parser finds can be modified by a plugin -(see "Node Visitors" below). - -The event object will contain the following properties: - -- filename: the name of the file -- comment: the comment associated with the symbol, if any -- id: the unique id of the symbol -- lineno: the line number the symbols was found on -- range: an array containing the first and last characters of the code - associated with the symbol -- astnode: the node of the parse tree -- code: information about the code. This usually contains "name", "type", and - "node" properties and might also have "value", "paramnames", or "funcscope" - properties depending on the symbol. - -#### Event: newDoclet - -This is the highest level event and is fired when a new doclet has been created. -This means that a jsdoc or a symbol has been processed and the actual doclet -that will be passed to the template has been created. - -The event object will contain the following properties: - -- doclet: the new doclet that was created - -The properties of the doclet can vary depending on the comment or symbol used to -create it. Additionally, tag definitions (See "Tag Definitions" below) can -modify the doclet. Some common properties you're likely to see include: - -- comment: the text of the comment (may be empty if symbol is undocumented) -- meta: some information about the doclet, like filename, line number, etc. -- description -- kind -- name -- longname: the fully qualified name, including memberof info -- memberof: the function/class/namespace that this is a member of -- scope: (global|static|instance|inner) -- undocumented: true if the symbol didn't have a jsdoc comment -- defaultvalue: the specified default value for a property/variable -- type: the specified type of parameter/property/function return (e.g. Boolean) -- params: an object containing the list of parameters to a function -- tags: an object containing the set of tags not handled by the parser (note: - this is only available if ```allowUnknownTags``` is set to true in the conf.json - file for JSDoc3) - -Below is an example of a newDoclet handler that shouts the descriptions: - - exports.handlers = { - newDoclet: function(e) { - // e.doclet will refer to the newly created doclet - // you can read and modify properties of that doclet if you wish - if (typeof e.doclet.description === 'string') { - e.doclet.description = e.doclet.description.toUpperCase(); - } - } - }; - -#### Event: fileComplete - -This is fired when the parser is done with a file. You might use this to -perform some cleanup for your plugin. - -The event object will contain the following properties: - -- filename: the name of the file -- source: the contents of the file - -### Tag Definitions - -Adding tags to the tag dictionary is a mid-level way to affect documentation -generation. Before a newDoclet event is triggered, jsdoc comment blocks are -parsed to determine the description and any jsdoc tags that may be present. When -a tag is found, if it has been defined in the tag dictionary, it is given a -chance to modify the doclet. - -Plugins can define tags by exporting a _defineTags_ function. That function will -be passed a dictionary that can be used to define tags, like so: - - exports.defineTags = function(dictionary) { - //define tags here - } - -#### The Dictionary - -The dictionary provides the following methods: - -1. defineTag(title, opts) - Used to define tags. - The first parameter is the name of the tag (e.g. "param" or "overview"). the - second is an object containing options for the tag. The options can be the - following: - - mustHaveValue (Boolean): whether or not the tag must have a value - (e.g "@name TheName") - - mustNotHaveValue (Boolean): whether or not the tag must not have a value - - canHaveType (Boolean): Whether or not the tag can have a type - (e.g. "@param **{String}** name the description of name") - - canHaveName (Boolean): Whether or not the tag can have a name - (e.g. "@param {String} **name** the description of name") - - isNamespace (Boolean): Whether or not the tag marks a doclet as representing - a namespace. The "@module" tag, for instance, sets this to true. - - onTagged (Function): A callback function executed when the tag is found. The - function is passed two parameters: the doclet and the tag. Here's an example: - - dictionary.defineTag('instance', { - onTagged: function(doclet, tag) { - doclet.scope = "instance"; - } - }); - The defineTag method returns a Tag. The Tag object has a method "synonym" - that can be used to declare synonyms to the tag. For example: - - dictionary.defineTag('exception', { - - }) - .synonym('throws'); -2. lookUp(title) - Used to lookup a tag. Returns either the tag or false if it's not defined -3. isNamespace(kind) - Used to determine if a particular doclet type represents a namespace -4. normalise(title) - Used to find the canonical name of a tag. The name passed in might be that - name or a synonym - -### Node Visitors - -At the lowest level, plugin authors can process each node in the parse tree by -defining a node visitor that will visit each node, creating an opportunity to -do things like modify comments and trigger parser events for any arbitrary piece -of code. - -Plugins can define a node visitor by exporting a ```nodeVisitor``` object that -contains a ```visitNode``` function, like so: - - exports.nodeVisitor = { - visitNode: function(node, e, parser, currentSourceName) { - //do all sorts of crazy things here - } - } - -The function is called on each node with the following parameters: - -- node: the node of the parse tree -- e: the event. If the node is one that the parser handles, this will already - be populated with the same things described in the _symbolFound_ event above. - Otherwise, it will be an empty object on which to set various properties. -- parser: the parser -- currentSourceName: the name of the file being parsed - -#### Making things happen - -The primary reasons to implement a node visitor are to be able to document -things that aren't normally documented (like function calls that create classes) -or to auto generate documentation for code that isn't documented. For instance, -a plugin might look for calls to a "_trigger" method since it knows that means -an event is fired and then generate documentation for the event. - -To make things happen, the ```visitNode``` function should modify properties -of the event parameter. In general the goal is to construct a comment and then -get an event to fire. After the parser lets all of the node visitors have a -look at the node, it looks to see if the event object has a ```comment``` -property and an ```event``` property. If it has both, the event named in the event -property is fired. The event is usually "symbolFound" or "jsdocCommentFound", -but theoretically, a plugin could define its own events and handle them. - -#### Example - -Below is an example of what a plugin for documenting jQuery UI widgets might do. -jQuery UI uses a factory function call to create widget classes. The plugin -looks for that function call and creates a symbol with documentation. It also -looks for any "this._trigger" function calls and automatically creates -documentation for the events that are triggered: - - exports.nodeVisitor = { - visitNode: function(node, e, parser, currentSourceName) { - if (node.type === Token.OBJECTLIT && node.parent && node.parent.type === Token.CALL && isInWidgetFactory(node, 1)) { - var widgetName = node.parent.arguments.get(0).toSource(); - e.id = 'astnode' + node.hashCode(); // the id of the object literal node - e.comment = String(node.parent.jsDoc||''); - e.lineno = node.parent.getLineno(); - e.filename = currentSourceName; - e.astnode = node; - e.code = { - name: "" + widgetName.substring(1, widgetName.length() - 1), - type: "class", - node: node - }; - e.event = "symbolFound"; - e.finishers = [parser.addDocletRef]; - - addCommentTag(e, "param", "{Object=} options A set of configuration options"); - } - else if(isTriggerCall(node)) { - var nameNode = node.arguments.get(0); - eventName = String((nameNode.type == Token.STRING) ? nameNode.value : nameNode.toSource()), - func = {}, - comment = "@event\n", - eventKey = ""; - - if (node.enclosingFunction) { - func.id = 'astnode'+node.enclosingFunction.hashCode(); - func.doclet = parser.refs[func.id]; - } - if(func.doclet) { - func.doclet.addTag("fires", eventName); - if (func.doclet.memberof) { - eventKey = func.doclet.memberof + "#event:" + eventName; - comment += "@name " + func.doclet.memberof + "#" + eventName; - } - } - e.comment = comment; - e.lineno = node.getLineno(); - e.filename = currentSourceName; - e.event = "jsdocCommentFound"; - } - } - }; - function isTriggerCall(node) { - if(node.type != Token.CALL) { return false; } - var target = node.getTarget(), - left = target && target.left && String(target.left.toSource()), - right = target && target.right && String(target.right.toSource()); - return (left === "this" && right === "_trigger"); - } - - function isInWidgetFactory(node, depth) { - var parent = node.parent, - d = 0; - while(parent && (!depth || d < depth)) { - if (parent.type === Token.CALL) { - var target = parent.getTarget(), - left = target && target.left && String(target.left.toSource()), - right = target && target.right && String(target.right.toSource()); - return ((left === "$" || left === "jQuery") && right === "widget"); - } else { - parent = parent.parent; - d++; - } - } - return false; - } - -You'll notice a "finishers" property set. The finishers property should contain -an array of functions to be called after the event is fired and all the handlers -have processed it. The parser provides an ```addDocletRef``` function that adds the -doclet to the map (keyed off of the id property) of doclets it knows about. - -Lastly, the visitors are executed in the order the plugins are listed in the -conf.json file. A plugin can stop later plugins from visiting a node by -setting a ```stopPropagation``` property on the event object (e.stopPropagation = true). -A plugin can stop the event from firing setting a ```preventDefault``` property. - -### Throwing Errors - -If you wish your plugin to throw an error, do it using the `handle` function in -the `jsdoc/util/error` module: - - require('jsdoc/util/error').handle( new Error('I do not like green eggs and ham!') ); - -By default, this will throw the error, halting the execution of JSDoc. However, -if the user enabled JSDoc's `--lenient` switch, JSDoc will simply log the error -to the console and continue. - -Packaging JSDoc 3 Plugins ----- - -The JSDoc 3 Jakefile has an ```install``` task that can be used to install a -plugin into the JSDoc directory. So running the following will install the -plugin: - - $>jake install[path/to/YourPluginFolder] - -**Note**: On some operating systems, including OS X, you may need to quote the -target name and parameters: - - $>jake 'install[path/to/YourPluginFolder]' - -The task is passed a directory that should look something like the following: - - YourPluginFolder - |- plugins - | |- YourPlugin.js - | \- test - | |- fixtures - | | \- YourFixtures.js - | \- specs - | \- YourTests.js - \- templates - \- YourTemplate - \- publish.js diff --git a/plugins/sourcetag.js b/plugins/sourcetag.js index 47de68e4..a92969d8 100644 --- a/plugins/sourcetag.js +++ b/plugins/sourcetag.js @@ -4,6 +4,8 @@ */ 'use strict'; +var logger = require('jsdoc/util/logger'); + exports.handlers = { /** Support @source tag. Expected value like: @@ -32,7 +34,8 @@ exports.handlers = { value = JSON.parse(tag.value); } catch(e) { - throw new Error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.'); + logger.error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.'); + return; } e.doclet.meta = e.doclet.meta || {}; @@ -41,4 +44,4 @@ exports.handlers = { } } } -}; \ No newline at end of file +}; diff --git a/plugins/test/specs/verboseOutput.js b/plugins/test/specs/verboseOutput.js deleted file mode 100644 index 5f7d508a..00000000 --- a/plugins/test/specs/verboseOutput.js +++ /dev/null @@ -1,24 +0,0 @@ -/*global describe: true, env: true, expect: true, it: true, jasmine: true, xit: true */ -/** - * @author Rob Taylor [manix84@gmail.com] - */ - -var path = require('jsdoc/path'); - -describe("verbose output plugin", function () { - var parser = jasmine.createParser(); - var path = require('jsdoc/path'); - - var docSet; - var pluginPath = 'plugins/verboseOutput'; - var plugin = require( path.resolve(env.dirname, pluginPath) ); - - //require('jsdoc/plugins').installPlugins(['plugins/verboseOutput'], parser); - docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser); - - xit("should log file names to console", function() { - // TODO: this doesn't actually test the plugin... - var fileBegin = docSet.getByLongname("module:plugins/verboseOutput.handlers.fileBegin"); - expect(fileBegin[0].description).toEqual("Logging the file name to the console."); - }); -}); diff --git a/plugins/verboseOutput.js b/plugins/verboseOutput.js deleted file mode 100644 index af145429..00000000 --- a/plugins/verboseOutput.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Adds a verbose output to the console, so that you can see what's happening in your process. - * @module plugins/verboseOutput - * @author Rob Taylor - The basic idea - * @author Michael Mathews - Wrote the first itteration with me :) - */ -'use strict'; - -exports.handlers = { - /** - * Logging the file name to the console. - */ - fileBegin: function (data) { - console.log(data.filename); - } -}; diff --git a/rhino/assert.js b/rhino/assert.js new file mode 100644 index 00000000..5c762d07 --- /dev/null +++ b/rhino/assert.js @@ -0,0 +1,315 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// UTILITY +var util = require('util'); +var pSlice = Array.prototype.slice; + +// 1. The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. + +var assert = module.exports = ok; + +// 2. The AssertionError is defined in assert. +// new assert.AssertionError({ message: message, +// actual: actual, +// expected: expected }) + +assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + this.message = options.message || getMessage(this); +}; + +// assert.AssertionError instanceof Error +util.inherits(assert.AssertionError, Error); + +function replacer(key, value) { + if (util.isUndefined(value)) { + return '' + value; + } + if (util.isNumber(value) && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (util.isFunction(value) || util.isRegExp(value)) { + return value.toString(); + } + return value; +} + +function truncate(s, n) { + if (util.isString(s)) { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } +} + +function getMessage(self) { + return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' + + self.operator + ' ' + + truncate(JSON.stringify(self.expected, replacer), 128); +} + +// At present only the three keys mentioned above are used and +// understood by the spec. Implementations or sub modules can pass +// other keys to the AssertionError's constructor - they will be +// ignored. + +// 3. All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. + +function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction + }); +} + +// EXTENSION! allows for well behaved errors defined elsewhere. +assert.fail = fail; + +// 4. Pure assertion tests whether a value is truthy, as determined +// by !!guard. +// assert.ok(guard, message_opt); +// This statement is equivalent to assert.equal(true, !!guard, +// message_opt);. To test strictly for the value true, use +// assert.strictEqual(true, guard, message_opt);. + +function ok(value, message) { + if (!value) fail(value, true, message, '==', assert.ok); +} +assert.ok = ok; + +// 5. The equality assertion tests shallow, coercive equality with +// ==. +// assert.equal(actual, expected, message_opt); + +assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); +}; + +// 6. The non-equality assertion tests for whether two objects are not equal +// with != assert.notEqual(actual, expected, message_opt); + +assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } +}; + +// 7. The equivalence assertion tests a deep equality relation. +// assert.deepEqual(actual, expected, message_opt); + +assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } +}; + +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + + } else if (util.isBuffer(actual) && util.isBuffer(expected)) { + if (actual.length != expected.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } + + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (util.isDate(actual) && util.isDate(expected)) { + return actual.getTime() === expected.getTime(); + + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (util.isRegExp(actual) && util.isRegExp(expected)) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; + + // 7.4. Other pairs that do not both pass typeof value == 'object', + // equivalence is determined by ==. + } else if (!util.isObject(actual) && !util.isObject(expected)) { + return actual == expected; + + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} + +function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; +} + +function objEquiv(a, b) { + if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b)) + return false; + // an identical 'prototype' property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + try { + var ka = Object.keys(a), + kb = Object.keys(b), + key, i; + } catch (e) {//happens when one is a string literal and the other isn't + return false; + } + // having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) return false; + } + return true; +} + +// 8. The non-equivalence assertion tests for any deep inequality. +// assert.notDeepEqual(actual, expected, message_opt); + +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } +}; + +// 9. The strict equality assertion tests strict equality, as determined by ===. +// assert.strictEqual(actual, expected, message_opt); + +assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } +}; + +// 10. The strict non-equality assertion tests for strict inequality, as +// determined by !==. assert.notStrictEqual(actual, expected, message_opt); + +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (Object.prototype.toString.call(expected) == '[object RegExp]') { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } + + return false; +} + +function _throws(shouldThrow, block, expected, message) { + var actual; + + if (util.isString(expected)) { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + + (message ? ' ' + message : '.'); + + if (shouldThrow && !actual) { + fail(actual, expected, 'Missing expected exception' + message); + } + + if (!shouldThrow && expectedException(actual, expected)) { + fail(actual, expected, 'Got unwanted exception' + message); + } + + if ((shouldThrow && actual && expected && + !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } +} + +// 11. Expected to throw an error: +// assert.throws(block, Error_opt, message_opt); + +assert.throws = function(block, /*optional*/error, /*optional*/message) { + _throws.apply(this, [true].concat(pSlice.call(arguments))); +}; + +// EXTENSION! This is annoying to write outside this module. +assert.doesNotThrow = function(block, /*optional*/message) { + _throws.apply(this, [false].concat(pSlice.call(arguments))); +}; + +assert.ifError = function(err) { if (err) {throw err;}}; diff --git a/rhino/rhino-shim.js b/rhino/rhino-shim.js index bcaf6620..125fc01e 100644 --- a/rhino/rhino-shim.js +++ b/rhino/rhino-shim.js @@ -16,15 +16,18 @@ global.setTimeout = null; global.clearTimeout = null; global.setInterval = null; global.clearInterval = null; +global.setImmediate = null; +global.clearImmediate = null; (function() { 'use strict'; - // TODO: tune number of threads if necessary - var timerPool = new java.util.concurrent.ScheduledThreadPoolExecutor(10); + var timerPool = new java.util.concurrent.ScheduledThreadPoolExecutor(1); var timers = {}; var timerCount = 1; var timerUnits = java.util.concurrent.TimeUnit.MILLISECONDS; + var queue = {}; + var queueActive = false; function getCallback(fn) { return new java.lang.Runnable({ @@ -54,6 +57,41 @@ global.clearInterval = null; }; global.clearInterval = global.clearTimeout; + + // adapted from https://github.com/alexgorbatchev/node-browser-builtins + // MIT license + global.setImmediate = (function() { + function drain() { + var key; + + var keys = Object.keys(queue); + + queueActive = false; + + for (var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + var fn = queue[key]; + delete queue[key]; + fn(); + } + } + + return function(fn) { + var timerId = timerCount++; + queue[timerId] = fn; + + if (!queueActive) { + queueActive = true; + global.setTimeout(drain, 0); + } + + return timerId; + }; + })(); + + global.clearImmediate = function(id) { + delete queue[id]; + }; })(); /** diff --git a/templates/default/publish.js b/templates/default/publish.js index a8a386b1..ce01ee36 100644 --- a/templates/default/publish.js +++ b/templates/default/publish.js @@ -5,7 +5,7 @@ var template = require('jsdoc/template'), fs = require('jsdoc/fs'), path = require('jsdoc/path'), taffy = require('taffydb').taffy, - handle = require('jsdoc/util/error').handle, + logger = require('jsdoc/util/logger'), helper = require('jsdoc/util/templateHelper'), htmlsafe = helper.htmlsafe, linkto = helper.linkto, @@ -141,7 +141,7 @@ function generateSourceFiles(sourceFiles, encoding) { }; } catch(e) { - handle(e); + logger.error('Error while generating source file %s: %s', file, e.message); } generate('Source: ' + sourceFiles[file].shortened, [source], sourceOutfile, diff --git a/test/runner.js b/test/runner.js index 121df004..8a2b5cc0 100644 --- a/test/runner.js +++ b/test/runner.js @@ -7,6 +7,7 @@ * 4. Run Jasmine on each directory */ var fs = require('jsdoc/fs'); +var logger = require('jsdoc/util/logger'); var path = require('path'); fs.existsSync = fs.existsSync || path.existsSync; @@ -47,6 +48,9 @@ function testedAllParsers() { var runNextFolder = module.exports = function(callback) { testsCompleteCallback = testsCompleteCallback || callback; + // silence the logger while we run the tests + logger.setLevel(logger.LEVELS.SILENT); + if (index < specFolders.length) { // we need to run the test specs once for each parser // TODO: We currently support testing one parser per runtime diff --git a/test/specs/jsdoc/opts/args.js b/test/specs/jsdoc/opts/args.js index bf924547..d3c81839 100644 --- a/test/specs/jsdoc/opts/args.js +++ b/test/specs/jsdoc/opts/args.js @@ -1,184 +1,184 @@ /*global describe: true, expect: true, it: true */ -describe("jsdoc/opts/args", function() { +describe('jsdoc/opts/args', function() { var args = require('jsdoc/opts/args'); var querystring = require('querystring'); - it("should exist", function() { + it('should exist', function() { expect(args).toBeDefined(); - expect(typeof args).toEqual("object"); + expect(typeof args).toBe('object'); }); - it("should export a 'parse' function", function() { + it('should export a "parse" function', function() { expect(args.parse).toBeDefined(); - expect(typeof args.parse).toEqual("function"); + expect(typeof args.parse).toBe('function'); }); - it("should export a 'help' function", function() { + it('should export a "help" function', function() { expect(args.help).toBeDefined(); - expect(typeof args.help).toEqual("function"); + expect(typeof args.help).toBe('function'); }); - it("should export a 'get' function", function() { + it('should export a "get" function', function() { expect(args.get).toBeDefined(); - expect(typeof args.get).toEqual("function"); + expect(typeof args.get).toBe('function'); }); - describe("parse", function() { - it("should accept a '-t' option and return an object with a 'template' property", function() { + describe('parse', function() { + it('should accept a "-t" option and return an object with a "template" property', function() { args.parse(['-t', 'mytemplate']); var r = args.get(); - expect(r.template).toEqual('mytemplate'); + expect(r.template).toBe('mytemplate'); }); - it("should accept a '--template' option and return an object with a 'template' property", function() { + it('should accept a "--template" option and return an object with a "template" property', function() { args.parse(['--template', 'mytemplate']); var r = args.get(); - expect(r.template).toEqual('mytemplate'); + expect(r.template).toBe('mytemplate'); }); - it("should accept a '-c' option and return an object with a 'configure' property", function() { + it('should accept a "-c" option and return an object with a "configure" property', function() { args.parse(['-c', 'myconf.json']); var r = args.get(); - expect(r.configure).toEqual('myconf.json'); + expect(r.configure).toBe('myconf.json'); }); - it("should accept a '--configure' option and return an object with a 'configure' property", function() { + it('should accept a "--configure" option and return an object with a "configure" property', function() { args.parse(['--configure', 'myconf.json']); var r = args.get(); - expect(r.configure).toEqual('myconf.json'); + expect(r.configure).toBe('myconf.json'); }); - it("should accept a '-e' option and return an object with a 'encoding' property", function() { + it('should accept an "-e" option and return an object with a "encoding" property', function() { args.parse(['-e', 'ascii']); var r = args.get(); - expect(r.encoding).toEqual('ascii'); + expect(r.encoding).toBe('ascii'); }); - it("should accept a '--encoding' option and return an object with a 'encoding' property", function() { + it('should accept an "--encoding" option and return an object with an "encoding" property', function() { args.parse(['--encoding', 'ascii']); var r = args.get(); - expect(r.encoding).toEqual('ascii'); + expect(r.encoding).toBe('ascii'); }); - it("should accept a '-T' option and return an object with a 'test' property", function() { + it('should accept a "-T" option and return an object with a "test" property', function() { args.parse(['-T']); var r = args.get(); - expect(r.test).toEqual(true); + expect(r.test).toBe(true); }); - it("should accept a '--test' option and return an object with a 'test' property", function() { + it('should accept a "--test" option and return an object with a "test" property', function() { args.parse(['--test']); var r = args.get(); - expect(r.test).toEqual(true); + expect(r.test).toBe(true); }); - it("should accept a '-d' option and return an object with a 'destination' property", function() { + it('should accept a "-d" option and return an object with a "destination" property', function() { args.parse(['-d', 'mydestination']); var r = args.get(); - expect(r.destination).toEqual('mydestination'); + expect(r.destination).toBe('mydestination'); }); - it("should accept a '--destination' option and return an object with a 'destination' property", function() { + it('should accept a "--destination" option and return an object with a "destination" property', function() { args.parse(['--destination', 'mydestination']); var r = args.get(); - expect(r.destination).toEqual('mydestination'); + expect(r.destination).toBe('mydestination'); }); - it("should accept a '-p' option and return an object with a 'private' property", function() { + it('should accept a "-p" option and return an object with a "private" property', function() { args.parse(['-p']); var r = args.get(); - expect(r['private']).toEqual(true); + expect(r.private).toBe(true); }); - it("should accept a '--private' option and return an object with a 'private' property", function() { + it('should accept a "--private" option and return an object with a "private" property', function() { args.parse(['--private']); var r = args.get(); - expect(r['private']).toEqual(true); + expect(r.private).toBe(true); }); - it("should accept a '-r' option and return an object with a 'recurse' property", function() { + it('should accept a "-r" option and return an object with a "recurse" property', function() { args.parse(['-r']); var r = args.get(); - expect(r.recurse).toEqual(true); + expect(r.recurse).toBe(true); }); - it("should accept a '--recurse' option and return an object with a 'recurse' property", function() { + it('should accept a "--recurse" option and return an object with a "recurse" property', function() { args.parse(['--recurse']); var r = args.get(); - expect(r.recurse).toEqual(true); + expect(r.recurse).toBe(true); }); - it("should accept a '-l' option and return an object with a 'lenient' property", function() { + it('should accept a "-l" option and ignore it', function() { args.parse(['-l']); var r = args.get(); - expect(r.lenient).toEqual(true); + expect(r.lenient).not.toBeDefined(); }); - it("should accept a '--lenient' option and return an object with a 'lenient' property", function() { + it('should accept a "--lenient" option and ignore it', function() { args.parse(['--lenient']); var r = args.get(); - expect(r.lenient).toEqual(true); + expect(r.lenient).not.toBeDefined(); }); - it("should accept a '-h' option and return an object with a 'help' property", function() { + it('should accept a "-h" option and return an object with a "help" property', function() { args.parse(['-h']); var r = args.get(); - expect(r.help).toEqual(true); + expect(r.help).toBe(true); }); - it("should accept a '--help' option and return an object with a 'help' property", function() { + it('should accept a "--help" option and return an object with a "help" property', function() { args.parse(['--help']); var r = args.get(); - expect(r.help).toEqual(true); + expect(r.help).toBe(true); }); - it("should accept a '-X' option and return an object with a 'explain' property", function() { + it('should accept an "-X" option and return an object with an "explain" property', function() { args.parse(['-X']); var r = args.get(); - expect(r.explain).toEqual(true); + expect(r.explain).toBe(true); }); - it("should accept a '--explain' option and return an object with a 'explain' property", function() { + it('should accept an "--explain" option and return an object with an "explain" property', function() { args.parse(['--explain']); var r = args.get(); - expect(r.explain).toEqual(true); + expect(r.explain).toBe(true); }); - it("should accept a '-q' option and return an object with a 'query' property", function() { + it('should accept a "-q" option and return an object with a "query" property', function() { args.parse(['-q', 'foo=bar&fab=baz']); var r = args.get(); expect(r.query).toEqual({ foo: 'bar', fab: 'baz' }); }); - it("should accept a '--query' option and return an object with a 'query' property", function() { + it('should accept a "--query" option and return an object with a "query" property', function() { args.parse(['--query', 'foo=bar&fab=baz']); var r = args.get(); expect(r.query).toEqual({ foo: 'bar', fab: 'baz' }); }); - it("should use type coercion on the 'query' property so it has real booleans and numbers", function() { + it('should use type coercion on the "query" property so it has real booleans and numbers', function() { var obj = { foo: 'fab', bar: true, @@ -191,63 +191,77 @@ describe("jsdoc/opts/args", function() { expect(r.query).toEqual(obj); }); - it("should accept a '-t' option and return an object with a 'tutorials' property", function() { - args.parse(['-d', 'mytutorials']); + it('should accept a "-u" option and return an object with a "tutorials" property', function() { + args.parse(['-u', 'mytutorials']); var r = args.get(); - expect(r.destination).toEqual('mytutorials'); + expect(r.tutorials).toBe('mytutorials'); }); - it("should accept a '--tutorials' option and return an object with a 'tutorials' property", function() { + it('should accept a "--tutorials" option and return an object with a "tutorials" property', function() { args.parse(['--tutorials', 'mytutorials']); var r = args.get(); - expect(r.tutorials).toEqual('mytutorials'); + expect(r.tutorials).toBe('mytutorials'); }); - it("should accept a '--verbose' option and return an object with a 'verbose' property", function() { + it('should accept a "--debug" option and return an object with a "debug" property', function() { + args.parse(['--debug']); + var r = args.get(); + + expect(r.debug).toBe(true); + }); + + it('should accept a "--verbose" option and return an object with a "verbose" property', function() { args.parse(['--verbose']); var r = args.get(); - expect(r.verbose).toEqual(true); + expect(r.verbose).toBe(true); }); - it("should accept a '--match' option and return an object with a 'match' property", function() { + it('should accept a "--pedantic" option and return an object with a "pedantic" property', function() { + args.parse(['--pedantic']); + var r = args.get(); + + expect(r.pedantic).toBe(true); + }); + + it('should accept a "--match" option and return an object with a "match" property', function() { args.parse(['--match', '.*tag']); var r = args.get(); - expect(r.match).toEqual('.*tag'); + expect(r.match).toBe('.*tag'); }); - it("should accept multiple '--match' options and return an object with a 'match' property", function() { + it('should accept multiple "--match" options and return an object with a "match" property', function() { args.parse(['--match', '.*tag', '--match', 'parser']); var r = args.get(); expect(r.match).toEqual(['.*tag', 'parser']); }); - it("should accept a '--nocolor' option and return an object with a 'nocolor' property", function() { + it('should accept a "--nocolor" option and return an object with a "nocolor" property', function() { args.parse(['--nocolor']); var r = args.get(); - expect(r.nocolor).toEqual(true); + expect(r.nocolor).toBe(true); }); - it("should accept a '-v' option and return an object with a 'version' property", function() { + it('should accept a "-v" option and return an object with a "version" property', function() { args.parse(['-v']); var r = args.get(); - expect(r.version).toEqual(true); + expect(r.version).toBe(true); }); - it("should accept a '--version' option and return an object with a 'version' property", function() { + it('should accept a "--version" option and return an object with a "version" property', function() { args.parse(['--version']); var r = args.get(); - expect(r.version).toEqual(true); + expect(r.version).toBe(true); }); - it("should accept a naked option (i.e. no '-') and return an object with a '_' property", function() { + it('should accept a naked option (with no "-") and return an object with a "_" property', function() { args.parse(['myfile1', 'myfile2']); var r = args.get(); diff --git a/test/specs/jsdoc/tag.js b/test/specs/jsdoc/tag.js index 383a20e5..b80052a4 100644 --- a/test/specs/jsdoc/tag.js +++ b/test/specs/jsdoc/tag.js @@ -1,10 +1,11 @@ -/*global afterEach: true, describe: true, env: true, expect: true, it: true, spyOn: true */ +/*global afterEach, beforeEach, describe, env, expect, it, spyOn */ describe("jsdoc/tag", function() { var jsdoc = { tag: require('jsdoc/tag'), dictionary: require('jsdoc/tag/dictionary'), type: require('jsdoc/tag/type') }; + var logger = require('jsdoc/util/logger'); it('should exist', function() { expect(jsdoc.tag).toBeDefined(); @@ -151,28 +152,12 @@ describe("jsdoc/tag", function() { // further tests for this sort of thing are in jsdoc/tag/validator.js tests. describe("tag validating", function() { - var lenient = !!env.opts.lenient; - - function badTag() { - var tag = new jsdoc.tag.Tag("name"); - return tag; - } + it("logs an error for bad tags", function() { + spyOn(logger, 'error'); - afterEach(function() { - env.opts.lenient = lenient; - }); + var tag = new jsdoc.tag.Tag('param', '{!*!*!*!} foo'); - it("throws an exception for bad tags if the lenient option is not enabled", function() { - env.opts.lenient = false; - - expect(badTag).toThrow(); - }); - - it("doesn't throw an exception for bad tags if the lenient option is enabled", function() { - spyOn(console, 'log'); - env.opts.lenient = true; - - expect(badTag).not.toThrow(); + expect(logger.error).toHaveBeenCalled(); }); }); }); diff --git a/test/specs/jsdoc/tag/validator.js b/test/specs/jsdoc/tag/validator.js index ec42c4ab..ceca94e6 100644 --- a/test/specs/jsdoc/tag/validator.js +++ b/test/specs/jsdoc/tag/validator.js @@ -1,6 +1,8 @@ -/*global afterEach: true, beforeEach: true, describe: true, env: true, expect: true, it: true, spyOn: true */ +/*global afterEach, beforeEach, describe, env, expect, it, spyOn */ describe('jsdoc/tag/validator', function() { var validator = require('jsdoc/tag/validator'), + doop = require('jsdoc/util/doop'), + logger = require('jsdoc/util/logger'), tag = require('jsdoc/tag'); it('should exist', function() { @@ -13,11 +15,8 @@ describe('jsdoc/tag/validator', function() { expect(typeof validator.validate).toBe('function'); }); - // Note: various Error classes are private so we just test whether *any* - // error was thrown, not against particular types (e.g. UnknownTagError). describe('validate', function() { var dictionary = require('jsdoc/tag/dictionary'), - lenient = !!env.opts.lenient, allowUnknown = !!env.conf.tags.allowUnknownTags, badTag = {title: 'lkjasdlkjfb'}, meta = {filename: 'asdf.js', lineno: 1}, @@ -25,72 +24,53 @@ describe('jsdoc/tag/validator', function() { goodTag2 = new tag.Tag('ignore', '', meta); // mustNotHaveValue function validateTag(tag) { - return function() { validator.validate(tag, dictionary.lookUp(tag.title), meta); }; - } + validator.validate(tag, dictionary.lookUp(tag.title), meta); + } beforeEach(function() { - spyOn(console, 'log'); + spyOn(logger, 'error'); }); afterEach(function() { - env.opts.lenient = lenient; env.conf.tags.allowUnknownTags = allowUnknown; }); - it("throws an error if the tag is not in the dictionary, conf.tags.allowUnknownTags is false and lenient is false", function() { - env.opts.lenient = false; + it("logs an error if the tag is not in the dictionary and conf.tags.allowUnknownTags is false", function() { env.conf.tags.allowUnknownTags = false; - expect(validateTag(badTag)).toThrow(); + validateTag(badTag); + + expect(logger.error).toHaveBeenCalled(); }); - it("throws NO error if the tag is not in the dictionary, conf.tags.allowUnknownTags is false and lenient is true", function() { - env.opts.lenient = true; - env.conf.tags.allowUnknownTags = false; - expect(validateTag(badTag)).not.toThrow(); - }); - - it("doesn't throw an error if the tag is not in the dictionary and conf.tags.allowUnknownTags is true, regardless of lenience", function() { - // if it doesn't throw an error with lenient false, then it won't throw it with lenient true (we have - // tested lenient already in util/error.js) - env.opts.lenient = false; + it("doesn't log an error if the tag is not in the dictionary and conf.tags.allowUnknownTags is true", function() { env.conf.tags.allowUnknownTags = true; - expect(validateTag(badTag)).not.toThrow(); + validateTag(badTag); + + expect(logger.error).not.toHaveBeenCalled(); }); - it("throws no error for valid tags", function() { - env.opts.lenient = false; - expect(validateTag(goodTag)).not.toThrow(); - expect(validateTag(goodTag2)).not.toThrow(); + it("logs no error for valid tags", function() { + validateTag(goodTag); + validateTag(goodTag2); + + expect(logger.error).not.toHaveBeenCalled(); }); - it("throws an error if the tag has no text but .mustHaveValue is true and lenient is false, or none if it's true", function() { - // the name tag has .mustHaveValue. - var oldText = goodTag.text; - delete goodTag.text; + it("logs an error if the tag has no text but .mustHaveValue is true", function() { + var missingName = doop(goodTag); + missingName.text = null; + validateTag(missingName); - env.opts.lenient = false; - expect(validateTag(goodTag)).toThrow(); - - env.opts.lenient = true; - expect(validateTag(goodTag)).not.toThrow(); - - goodTag.text = oldText; + expect(logger.error).toHaveBeenCalled(); }); - it("throws an error if the tag has text but .mustNotHaveValue is true and lenient is false, or none if it's true", function() { - var oldVal = goodTag2.mustNotHaveValue, - text = goodTag2.text; - goodTag2.mustNotHaveValue = true; - goodTag2.text = goodTag2.text || 'asdf'; + it("logs an error if the tag has text but .mustNotHaveValue is true", function() { + var missingText = doop(goodTag2); + missingText.mustNotHaveValue = true; + missingText.text = missingText.text || 'asdf'; + validateTag(missingText); - env.opts.lenient = false; - expect(validateTag(goodTag2)).toThrow(); - - env.opts.lenient = true; - expect(validateTag(goodTag2)).not.toThrow(); - - goodTag2.mustNotHaveValue = oldVal; - goodTag2.text = oldVal; + expect(logger.error).toHaveBeenCalled(); }); }); }); diff --git a/test/specs/jsdoc/tutorial/resolver.js b/test/specs/jsdoc/tutorial/resolver.js index 9a4434b2..1681a8b6 100644 --- a/test/specs/jsdoc/tutorial/resolver.js +++ b/test/specs/jsdoc/tutorial/resolver.js @@ -1,9 +1,8 @@ -/*global afterEach: true, beforeEach: true, describe: true, env: true, expect: true, it: true, -spyOn: true */ +/*global beforeEach, describe, env, expect, it, spyOn */ describe("jsdoc/tutorial/resolver", function() { + var logger = require('jsdoc/util/logger'); var resolver = require('jsdoc/tutorial/resolver'); var tutorial = require('jsdoc/tutorial'); - var lenient = !!env.opts.lenient; it("should exist", function() { expect(resolver).toBeDefined(); @@ -171,59 +170,30 @@ describe("jsdoc/tutorial/resolver", function() { // error reporting. describe("Error reporting", function() { - // Tests for error reporting. - function missingTutorial() { + beforeEach(function() { + spyOn(logger, 'error'); + spyOn(logger, 'warn'); + }); + + it("logs an error for missing tutorials", function() { resolver.load(env.dirname + "/test/tutorials/incomplete"); resolver.resolve(); - } - function duplicateNamedTutorials() { - // can't add a tutorial if another with its name has already been added + + expect(logger.error).toHaveBeenCalled(); + }); + + it("logs a warning for duplicate-named tutorials (e.g. test.md, test.html)", function() { resolver.addTutorial(tute); - } - function duplicateDefinedTutorials() { + + expect(logger.warn).toHaveBeenCalled(); + }); + + it("logs an error for tutorials defined twice in .json files", function() { // can't have a tutorial's metadata defined twice in .json files resolver.load(env.dirname + "/test/tutorials/duplicateDefined"); resolver.resolve(); - } - beforeEach(function() { - spyOn(console, 'log'); - }); - - afterEach(function() { - env.opts.lenient = lenient; - }); - - it("throws an exception for missing tutorials if the lenient option is not enabled", function() { - env.opts.lenient = false; - - expect(missingTutorial).toThrow(); - }); - - it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() { - env.opts.lenient = true; - - expect(missingTutorial).not.toThrow(); - }); - - it("throws an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() { - env.opts.lenient = false; - expect(duplicateNamedTutorials).toThrow(); - }); - - it("doesn't throw an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() { - env.opts.lenient = true; - expect(duplicateNamedTutorials).not.toThrow(); - }); - - it("throws an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() { - env.opts.lenient = false; - expect(duplicateDefinedTutorials).toThrow(); - }); - - it("doesn't throw an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() { - env.opts.lenient = true; - expect(duplicateDefinedTutorials).not.toThrow(); + expect(logger.error).toHaveBeenCalled(); }); }); diff --git a/test/specs/jsdoc/util/error.js b/test/specs/jsdoc/util/error.js index 4b77e789..9e464437 100644 --- a/test/specs/jsdoc/util/error.js +++ b/test/specs/jsdoc/util/error.js @@ -1,53 +1,29 @@ -/*global describe: true, env: true, it: true */ +/*global beforeEach, describe, expect, it, spyOn */ describe("jsdoc/util/error", function() { - var error = require('jsdoc/util/error'), - handle = error.handle; + var error = require('jsdoc/util/error'); + var handle = error.handle; + var logger = require('jsdoc/util/logger'); - it("should exist", function() { - expect(error).toBeDefined(); - expect(typeof error).toEqual("object"); - }); + it("should exist", function() { + expect(error).toBeDefined(); + expect(typeof error).toEqual("object"); + }); - it("should export a 'handle' function", function() { - expect(handle).toBeDefined(); - expect(typeof handle).toEqual("function"); - }); + it("should export a 'handle' function", function() { + expect(handle).toBeDefined(); + expect(typeof handle).toEqual("function"); + }); - describe("handle", function() { - /*jshint evil: true */ - var lenient = !!env.opts.lenient; + describe("handle", function() { + it('should not throw', function() { + expect(handle).not.toThrow(); + }); - function handleError() { - handle( new Error("foo") ); - } + it('should log messages with logger.error()', function() { + spyOn(logger, 'error'); + handle('test'); - function handleObject() { - handle( { foo: "bar", baz: "qux"} ); - } - - afterEach(function() { - env.opts.lenient = lenient; - }); - - it("should re-throw errors by default", function() { - expect(handleError).toThrow(); - }); - - it("should re-throw errors if lenient mode is not enabled", function() { - env.opts.lenient = false; - - expect(handleError).toThrow(); - }); - - it("should not re-throw errors if lenient mode is enabled", function() { - env.opts.lenient = true; - spyOn(console, 'log'); - - expect(handleError).not.toThrow(); - }); - - it("should still work if the 'e' param is not an instanceof Error", function() { - expect(handleObject).toThrow(); - }); - }); + expect(logger.error).toHaveBeenCalled(); + }); + }); }); diff --git a/test/specs/jsdoc/util/logger.js b/test/specs/jsdoc/util/logger.js new file mode 100644 index 00000000..804893fe --- /dev/null +++ b/test/specs/jsdoc/util/logger.js @@ -0,0 +1,200 @@ +/*global afterEach, describe, expect, it, jasmine */ +describe('jsdoc/util/logger', function() { + var logger = require('jsdoc/util/logger'); + + var loggerArgs = ['foo bar %s', 'hello']; + + it('should exist', function() { + expect(logger).toBeDefined(); + expect(typeof logger).toBe('object'); + }); + + it('should inherit from EventEmitter', function() { + var EventEmitter = require('events').EventEmitter; + + expect(logger instanceof EventEmitter).toBe(true); + }); + + it('should export a "debug" method', function() { + expect(logger.debug).toBeDefined(); + expect(typeof logger.debug).toBe('function'); + }); + + it('should export an "error" method', function() { + expect(logger.error).toBeDefined(); + expect(typeof logger.error).toBe('function'); + }); + + it('should export a "fatal" method', function() { + expect(logger.fatal).toBeDefined(); + expect(typeof logger.fatal).toBe('function'); + }); + + it('should export a "getLevel" method', function() { + expect(logger.getLevel).toBeDefined(); + expect(typeof logger.getLevel).toBe('function'); + }); + + it('should export an "info" method', function() { + expect(logger.info).toBeDefined(); + expect(typeof logger.info).toBe('function'); + }); + + it('should export a "LEVELS" object', function() { + expect(logger.LEVELS).toBeDefined(); + expect(typeof logger.LEVELS).toBe('object'); + }); + + it('should export a "setLevel" method', function() { + expect(logger.setLevel).toBeDefined(); + expect(typeof logger.setLevel).toBe('function'); + }); + + it('should export a "verbose" method', function() { + expect(logger.verbose).toBeDefined(); + expect(typeof logger.verbose).toBe('function'); + }); + + it('should export a "warn" method', function() { + expect(logger.warn).toBeDefined(); + expect(typeof logger.warn).toBe('function'); + }); + + // helpers for testing logger methods + function eventIsEmitted(name) { + var called = false; + + logger.once('logger:' + name, function() { + called = true; + }); + logger[name](); + + expect(called).toBe(true); + } + + function eventGetsArguments(name) { + var args; + + logger.once('logger:' + name, function() { + args = Array.prototype.slice.call(arguments, 0); + }); + logger[name](loggerArgs[0], loggerArgs[1]); + + expect(args).toBeDefined(); + expect( Array.isArray(args) ).toBe(true); + expect(args[0]).toBe(loggerArgs[0]); + expect(args[1]).toBe(loggerArgs[1]); + } + + describe('debug', function() { + var methodName = 'debug'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('error', function() { + var methodName = 'error'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('fatal', function() { + var methodName = 'fatal'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('getLevel', function() { + it('should return LEVELS.SILENT when we are running tests', function() { + expect( logger.getLevel() ).toBe(logger.LEVELS.SILENT); + }); + }); + + describe('info', function() { + var methodName = 'info'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('LEVELS', function() { + var LEVELS = logger.LEVELS; + + it('should include the correct properties', function() { + expect(LEVELS.VERBOSE).toBeDefined(); + expect(LEVELS.DEBUG).toBeDefined(); + expect(LEVELS.INFO).toBeDefined(); + expect(LEVELS.WARN).toBeDefined(); + expect(LEVELS.ERROR).toBeDefined(); + expect(LEVELS.SILENT).toBeDefined(); + }); + + it('should weight the logging levels correctly relative to one another', function() { + expect(LEVELS.VERBOSE).toBeGreaterThan(LEVELS.DEBUG); + expect(LEVELS.DEBUG).toBeGreaterThan(LEVELS.INFO); + expect(LEVELS.INFO).toBeGreaterThan(LEVELS.WARN); + expect(LEVELS.WARN).toBeGreaterThan(LEVELS.ERROR); + expect(LEVELS.ERROR).toBeGreaterThan(LEVELS.SILENT); + }); + }); + + describe('setLevel', function() { + var oldLevel = logger.getLevel(); + + afterEach(function() { + logger.setLevel(oldLevel); + }); + + it('should update the log level', function() { + logger.setLevel(logger.LEVELS.VERBOSE); + expect( logger.getLevel() ).toBe(logger.LEVELS.VERBOSE); + }); + }); + + describe('verbose', function() { + var methodName = 'verbose'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('warn', function() { + var methodName = 'warn'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); +}); diff --git a/test/specs/jsdoc/util/templateHelper.js b/test/specs/jsdoc/util/templateHelper.js index 1b466f0d..9deb1cb8 100644 --- a/test/specs/jsdoc/util/templateHelper.js +++ b/test/specs/jsdoc/util/templateHelper.js @@ -1,11 +1,11 @@ -/*global afterEach: true, beforeEach: true, describe: true, expect: true, env: true, it: true, -jasmine: true, spyOn: true, xdescribe: true */ +/*global afterEach, beforeEach, describe, expect, env, it, jasmine, spyOn */ var hasOwnProp = Object.prototype.hasOwnProperty; describe("jsdoc/util/templateHelper", function() { var helper = require('jsdoc/util/templateHelper'), doclet = require('jsdoc/doclet'), doop = require('jsdoc/util/doop'), + logger = require('jsdoc/util/logger'), resolver = require('jsdoc/tutorial/resolver'), taffy = require('taffydb').taffy; helper.registerLink('test', 'path/to/test.html'); @@ -141,18 +141,11 @@ describe("jsdoc/util/templateHelper", function() { }); it("setting tutorials to the root tutorial object lets lookups work", function() { - var lenient = !!env.opts.lenient; - spyOn(console, 'log'); - - // tutorial doesn't exist, we want to muffle that error - env.opts.lenient = true; - helper.setTutorials(resolver.root); spyOn(resolver.root, 'getByName'); helper.tutorialToUrl('asdf'); - expect(resolver.root.getByName).toHaveBeenCalled(); - env.opts.lenient = lenient; + expect(resolver.root.getByName).toHaveBeenCalled(); }); }); @@ -747,23 +740,6 @@ describe("jsdoc/util/templateHelper", function() { delete helper.longnameToUrl.MyClass; }); - - it("doesn't throw an error in lenient mode if a 'returns' item has no value", function() { - function getReturns() { - return helper.getSignatureReturns(doc); - } - - var doc; - var lenient = !!env.opts.lenient; - - env.opts.lenient = true; - spyOn(console, 'log'); - doc = new doclet.Doclet('/** @function myFunction\n@returns */', {}); - - expect(getReturns).not.toThrow(); - - env.opts.lenient = lenient; - }); }); describe("getAncestorLinks", function() { @@ -926,44 +902,32 @@ describe("jsdoc/util/templateHelper", function() { }); describe("tutorialToUrl", function() { - var lenient = !!env.opts.lenient; - function missingTutorial() { var url = helper.tutorialToUrl("be-a-perfect-person-in-just-three-days"); } beforeEach(function() { - spyOn(console, 'log'); + spyOn(logger, 'error'); helper.setTutorials(resolver.root); }); afterEach(function() { helper.setTutorials(null); - env.opts.lenient = lenient; }); - it('throws an exception if the tutorial is missing and the lenient option is not enabled', function() { - env.opts.lenient = false; - expect(missingTutorial).toThrow(); + it('logs an error if the tutorial is missing', function() { + helper.tutorialToUrl('be-a-perfect-person-in-just-three-days'); + + expect(logger.error).toHaveBeenCalled(); }); - it('does not throw an exception if the tutorial is missing and the lenient option is enabled', function() { - env.opts.lenient = true; + it("logs an error if the tutorial's name is a reserved JS keyword and it doesn't exist", function() { + helper.tutorialToUrl('prototype'); - expect(missingTutorial).not.toThrow(); - }); - - it("does not return a tutorial if its name is a reserved JS keyword and it doesn't exist", function() { - env.opts.lenient = false; - expect(function () { helper.tutorialToUrl('prototype'); }).toThrow(); + expect(logger.error).toHaveBeenCalled(); }); it("creates links to tutorials if they exist", function() { - // NOTE: we have to set lenient = true here because otherwise JSDoc will - // cry when trying to resolve the same set of tutorials twice (once - // for the tutorials tests, and once here). - env.opts.lenient = true; - // load the tutorials we already have for the tutorials tests resolver.load(env.dirname + "/test/tutorials/tutorials"); resolver.resolve(); @@ -985,34 +949,21 @@ describe("jsdoc/util/templateHelper", function() { }); describe("toTutorial", function() { - var lenient = !!env.opts.lenient; - - function missingParam() { - helper.toTutorial(); - } - - afterEach(function() { - env.opts.lenient = lenient; - helper.setTutorials(null); - }); - beforeEach(function () { + spyOn(logger, 'error'); helper.setTutorials(resolver.root); }); - it('throws an exception if the first param is missing and the lenient option is not enabled', function() { - env.opts.lenient = false; + afterEach(function() { + helper.setTutorials(null); + }); - expect(missingParam).toThrow(); + it('logs an error if the first param is missing', function() { + helper.toTutorial(); + + expect(logger.error).toHaveBeenCalled(); }); - it('does not throw an exception if the first param is missing and the lenient option is enabled', function() { - spyOn(console, 'log'); - env.opts.lenient = true; - - expect(missingParam).not.toThrow(); - }); - // missing tutorials it("returns the tutorial name if it's missing and no missingOpts is provided", function() { helper.setTutorials(resolver.root); @@ -1046,12 +997,6 @@ describe("jsdoc/util/templateHelper", function() { // now we do non-missing tutorials. it("returns a link to the tutorial if not missing", function() { - // NOTE: we have to set lenient = true here because otherwise JSDoc will - // cry when trying to resolve the same set of tutorials twice (once - // for the tutorials tests, and once here). - env.opts.lenient = true; - spyOn(console, 'log'); - // load the tutorials we already have for the tutorials tests resolver.load(env.dirname + "/test/tutorials/tutorials"); resolver.resolve(); diff --git a/test/specs/tags/paramtag.js b/test/specs/tags/paramtag.js index e4dd33ed..c72ddc40 100644 --- a/test/specs/tags/paramtag.js +++ b/test/specs/tags/paramtag.js @@ -78,14 +78,9 @@ describe("@param tag", function() { expect(commit.params[0].name).toBe('atomic'); }); - it('When a symbol has a @param tag with an invalid type expression, the doclet is generated in lenient mode, and the JSDoc comment is ignored.', function() { + it('When a symbol has a @param tag with an invalid type expression, the JSDoc comment is ignored.', function() { var badDocSet; var test; - var lenient = !!env.opts.lenient; - - env.opts.lenient = true; - spyOn(console, 'log'); - badDocSet = jasmine.getDocSetFromFile('test/fixtures/paramtaginvalidtype.js'); test = badDocSet.getByLongname('Test#test')[0]; @@ -99,7 +94,5 @@ describe("@param tag", function() { expect(test.meta.filename).toBe('[[string0]]'); expect(test.description).not.toBeDefined(); - - env.opts.lenient = lenient; }); });