From 359fa84d3c3865470d1f6f512e4cf7e5cb4f428d Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Mon, 15 Apr 2013 06:55:45 -0700 Subject: [PATCH] add plugin making it easier to link to overloaded methods (#179) --- plugins/overloadHelper.js | 184 ++++++++++++++++++++++++ plugins/test/fixtures/overloadHelper.js | 50 +++++++ plugins/test/specs/overloadHelper.js | 96 +++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 plugins/overloadHelper.js create mode 100644 plugins/test/fixtures/overloadHelper.js create mode 100644 plugins/test/specs/overloadHelper.js diff --git a/plugins/overloadHelper.js b/plugins/overloadHelper.js new file mode 100644 index 000000000..35fe32136 --- /dev/null +++ b/plugins/overloadHelper.js @@ -0,0 +1,184 @@ +/** + * The Overload Helper plugin automatically adds a signature-like string to the longnames of + * overloaded functions and methods. In JSDoc, this string is known as a _variation_. (The longnames + * of overloaded constructor functions are _not_ updated, so that JSDoc can identify the class' + * members correctly.) + * + * Using this plugin allows you to link to overloaded functions without manually adding `@variation` + * tags to your documentation. + * + * For example, suppose your code includes a function named `foo` that you can call in the + * following ways: + * + * + `foo()` + * + `foo(bar)` + * + `foo(bar, baz)` (where `baz` is repeatable) + * + * This plugin assigns the following variations and longnames to each version of `foo`: + * + * + `foo()` gets the variation `()` and the longname `foo()`. + * + `foo(bar)` gets the variation `(bar)` and the longname `foo(bar)`. + * + `foo(bar, baz)` (where `baz` is repeatable) gets the variation `(bar, ...baz)` and the longname + * `foo(bar, ...baz)`. + * + * You can then link to these functions with `{@link foo()}`, `{@link foo(bar)}`, and + * `{@link foo(bar, ...baz)`. Note that the variation is based on the names of the function + * parameters, _not_ their types. + * + * If you prefer to manually assign variations to certain functions, you can still do so with the + * `@variation` tag. This plugin will not change these variations or add more variations for that + * function, as long as the variations you've defined result in unique longnames. + * + * If an overloaded function includes multiple signatures with the same parameter names, the plugin + * will assign numeric variations instead, starting at `(1)` and counting upwards. + * + * @module plugins/overloadHelper + * @author Jeff Williams + * @license Apache License 2.0 + */ + +// lookup table of function doclets by longname +var functionDoclets; + +function hasUniqueValues(obj) { + var isUnique = true; + var seen = []; + Object.keys(obj).forEach(function(key) { + if (seen.indexOf(obj[key]) !== -1) { + isUnique = false; + } + + seen.push(obj[key]); + }); + + return isUnique; +} + +function getParamNames(params) { + var names = []; + + params.forEach(function(param) { + var name = param.name || ''; + if (param.variable) { + name = '...' + name; + } + if (name !== '') { + names.push(name); + } + }); + + return names.length ? names.join(', ') : ''; +} + +function getParamVariation(doclet) { + return getParamNames(doclet.params || []); +} + +function getUniqueVariations(doclets) { + var counter = 0; + var variations = {}; + var docletKeys = Object.keys(doclets); + + function getUniqueNumbers() { + var format = require('util').format; + + docletKeys.forEach(function(doclet) { + var newLongname; + + while (true) { + counter++; + variations[doclet] = String(counter); + + // is this longname + variation unique? + newLongname = format('%s(%s)', doclets[doclet].longname, variations[doclet]); + if ( !functionDoclets[newLongname] ) { + break; + } + } + }); + } + + function getUniqueNames() { + // start by trying to preserve existing variations + docletKeys.forEach(function(doclet) { + variations[doclet] = doclets[doclet].variation || getParamVariation(doclets[doclet]); + }); + + // if they're identical, try again, without preserving existing variations + if ( !hasUniqueValues(variations) ) { + docletKeys.forEach(function(doclet) { + variations[doclet] = getParamVariation(doclets[doclet]); + }); + + // if they're STILL identical, switch to numeric variations + if ( !hasUniqueValues(variations) ) { + getUniqueNumbers(); + } + } + } + + // are we already using numeric variations? if so, keep doing that + if (functionDoclets[doclets.newDoclet.longname + '(1)']) { + getUniqueNumbers(); + } + else { + getUniqueNames(); + } + + return variations; +} + +function ensureUniqueLongname(newDoclet) { + var doclets = { + oldDoclet: functionDoclets[newDoclet.longname], + newDoclet: newDoclet + }; + var docletKeys = Object.keys(doclets); + var oldDocletLongname; + var variations = {}; + + if (doclets.oldDoclet) { + oldDocletLongname = doclets.oldDoclet.longname; + // if the shared longname has a variation, like MyClass#myLongname(variation), + // remove the variation + if (doclets.oldDoclet.variation || doclets.oldDoclet.variation === '') { + docletKeys.forEach(function(doclet) { + doclets[doclet].longname = doclets[doclet].longname.replace(/\([\s\S]*\)$/, ''); + doclets[doclet].variation = null; + }); + } + + variations = getUniqueVariations(doclets); + + // update the longnames/variations + docletKeys.forEach(function(doclet) { + doclets[doclet].longname += '(' + variations[doclet] + ')'; + doclets[doclet].variation = variations[doclet]; + }); + + // update the old doclet in the lookup table + functionDoclets[oldDocletLongname] = null; + functionDoclets[doclets.oldDoclet.longname] = doclets.oldDoclet; + } + + // always store the new doclet in the lookup table + functionDoclets[doclets.newDoclet.longname] = doclets.newDoclet; + + return doclets.newDoclet; +} + +exports.handlers = { + parseBegin: function() { + functionDoclets = {}; + }, + + newDoclet: function(e) { + if (e.doclet.kind === 'function') { + e.doclet = ensureUniqueLongname(e.doclet); + } + }, + + parseComplete: function() { + functionDoclets = null; + } +}; diff --git a/plugins/test/fixtures/overloadHelper.js b/plugins/test/fixtures/overloadHelper.js new file mode 100644 index 000000000..12c298c62 --- /dev/null +++ b/plugins/test/fixtures/overloadHelper.js @@ -0,0 +1,50 @@ +/** + * A bowl of non-spicy soup. + * @class + *//** + * A bowl of spicy soup. + * @class + * @param {number} spiciness - The spiciness of the soup, in Scoville heat units (SHU). + */ +function Soup(spiciness) {} + +/** + * Slurp the soup. + *//** + * Slurp the soup loudly. + * @param {number} dBA - The slurping volume, in A-weighted decibels. + */ +Soup.prototype.slurp = function(dBA) {}; + +/** + * Salt the soup as needed, using a highly optimized soup-salting heuristic. + *//** + * Salt the soup, specifying the amount of salt to add. + * @variation mg + * @param {number} amount - The amount of salt to add, in milligrams. + */ +Soup.prototype.salt = function(amount) {}; + +/** + * Heat the soup by the specified number of degrees. + * @param {number} degrees - The number of degrees, in Fahrenheit, by which to heat the soup. + *//** + * Heat the soup by the specified number of degrees. + * @variation 1 + * @param {string} degrees - The number of degrees, in Fahrenheit, by which to heat the soup, but + * as a string for some reason. + *//** + * Heat the soup by the specified number of degrees. + * @param {boolean} degrees - The number of degrees, as a boolean. Wait, what? + */ +Soup.prototype.heat = function(degrees) {}; + +/** + * Discard the soup. + * @variation discardSoup + *//** + * Discard the soup by pouring it into the specified container. + * @variation discardSoup + * @param {Object} container - The container in which to discard the soup. + */ +Soup.prototype.discard = function(container) {}; diff --git a/plugins/test/specs/overloadHelper.js b/plugins/test/specs/overloadHelper.js new file mode 100644 index 000000000..c57d21306 --- /dev/null +++ b/plugins/test/specs/overloadHelper.js @@ -0,0 +1,96 @@ +/*global describe: true, expect: true, it: true, jasmine: true, xit: true */ +describe('plugins/overloadHelper', function() { + var parser = new (require('jsdoc/src/parser')).Parser(); + var plugin = require('plugins/overloadHelper'); + var docSet; + + require('jsdoc/plugins').installPlugins(['plugins/overloadHelper'], parser); + docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/overloadHelper.js', parser); + + it('should exist', function() { + expect(plugin).toBeDefined(); + expect(typeof plugin).toBe('object'); + }); + + it('should export handlers', function() { + expect(plugin.handlers).toBeDefined(); + expect(typeof plugin.handlers).toBe('object'); + }); + + it('should export a "newDoclet" handler', function() { + expect(plugin.handlers.newDoclet).toBeDefined(); + expect(typeof plugin.handlers.newDoclet).toBe('function'); + }); + + it('should export a "parseComplete" handler', function() { + expect(plugin.handlers.parseComplete).toBeDefined(); + expect(typeof plugin.handlers.parseComplete).toBe('function'); + }); + + describe('newDoclet handler', function() { + it('should not add unique longnames to constructors', function() { + var soup = docSet.getByLongname('Soup'); + var soup1 = docSet.getByLongname('Soup()'); + var soup2 = docSet.getByLongname('Soup(spiciness)'); + + expect(soup.length).toBe(2); + expect(soup1.length).toBe(0); + expect(soup2.length).toBe(0); + }); + + it('should add unique longnames to methods', function() { + var slurp = docSet.getByLongname('Soup#slurp'); + var slurp1 = docSet.getByLongname('Soup#slurp()'); + var slurp2 = docSet.getByLongname('Soup#slurp(dBA)'); + + expect(slurp.length).toBe(0); + expect(slurp1.length).toBe(1); + expect(slurp2.length).toBe(1); + }); + + it('should update the "variation" property of the method', function() { + var slurp1 = docSet.getByLongname('Soup#slurp()')[0]; + var slurp2 = docSet.getByLongname('Soup#slurp(dBA)')[0]; + + expect(slurp1.variation).toBe(''); + expect(slurp2.variation).toBe('dBA'); + }); + + it('should not add to or change existing variations that are unique', function() { + var salt1 = docSet.getByLongname('Soup#salt'); + var salt2 = docSet.getByLongname('Soup#salt(mg)'); + + expect(salt1.length).toBe(1); + expect(salt2.length).toBe(1); + }); + + it('should not duplicate the names of existing numeric variations', function() { + var heat1 = docSet.getByLongname('Soup#heat(1)'); + var heat2 = docSet.getByLongname('Soup#heat(2)'); + var heat3 = docSet.getByLongname('Soup#heat(3)'); + + expect(heat1.length).toBe(1); + expect(heat2.length).toBe(1); + expect(heat3.length).toBe(1); + }); + + it('should replace identical variations with new, unique variations', function() { + var discard1 = docSet.getByLongname('Soup#discard()'); + var discard2 = docSet.getByLongname('Soup#discard(container)'); + + expect(discard1.length).toBe(1); + expect(discard2.length).toBe(1); + }); + }); + + describe('parseComplete handler', function() { + // disabled because on the second run, each comment is being parsed twice; who knows why... + xit('should not retain parse results between parser runs', function() { + parser.clear(); + docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/overloadHelper.js', parser); + var heat = docSet.getByLongname('Soup#heat(4)'); + + expect(heat.length).toBe(0); + }); + }); +});