forked from jsdoc/jsdoc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add plugin making it easier to link to overloaded methods (jsdoc#179)
- Loading branch information
Showing
3 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <[email protected]> | ||
* @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; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); |