fix name resolution when the exports tag is used on a pointer to the module's exports object (#404)

This commit is contained in:
Jeff Williams 2014-10-11 09:58:51 -07:00
parent 1774569850
commit f77984df55
3 changed files with 97 additions and 26 deletions

View File

@ -17,6 +17,12 @@ var currentModule = null;
var SCOPE_PUNC = jsdoc.name.SCOPE.PUNC;
var unresolvedName = /^((?:module.)?exports|this)(\.|$)/;
function CurrentModule(doclet) {
this.doclet = doclet;
this.longname = doclet.longname;
this.originalName = doclet.meta.code.name || '';
}
function filterByLongname(doclet) {
// you can't document prototypes
if ( /#$/.test(doclet.longname) ) {
@ -76,13 +82,13 @@ function createSymbolDoclet(comment, e) {
function setCurrentModule(doclet) {
if (doclet.kind === 'module') {
currentModule = doclet.longname;
currentModule = new CurrentModule(doclet);
}
}
function setModuleScopeMemberOf(doclet) {
// handle module symbols that are _not_ assigned to module.exports
if (currentModule && currentModule !== doclet.name) {
if (currentModule && currentModule.longname !== doclet.name) {
// if we don't already know the scope, it must be an inner member
if (!doclet.scope) {
doclet.addTag('inner');
@ -91,7 +97,7 @@ function setModuleScopeMemberOf(doclet) {
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module
if (!doclet.memberof && doclet.scope !== 'global') {
doclet.addTag('memberof', currentModule);
doclet.addTag('memberof', currentModule.longname);
}
}
}
@ -133,41 +139,43 @@ function processAlias(parser, doclet, astNode) {
doclet.postProcess();
}
function findModuleMemberof(parser, doclet, astNode, nameStartsWith) {
// TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
var memberof = '';
var nameAndPunc = nameStartsWith + (trailingPunc || '');
var scopePunc = '';
// remove stuff that indicates module membership (but don't touch the name `module.exports`,
// which identifies the module object itself)
if (doclet.name !== 'module.exports') {
doclet.name = doclet.name.replace(unresolvedName, '');
doclet.name = doclet.name.replace(nameAndPunc, '');
}
// like /** @module foo */ exports.bar = 1;
// or /** @module foo */ module.exports.bar = 1;
// but not /** @module foo */ module.exports = 1;
if ( (nameStartsWith === 'exports' || nameStartsWith === 'module.exports') &&
doclet.name !== 'module.exports' && currentModule ) {
memberof = currentModule;
// like `bar` in:
// exports.bar = 1;
// module.exports.bar = 1;
// module.exports = MyModuleObject; MyModuleObject.bar = 1;
if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') {
memberof = currentModule.longname;
scopePunc = SCOPE_PUNC.STATIC;
}
// like: module.exports = 1;
else if (doclet.name === 'module.exports' && currentModule) {
doclet.addTag('name', currentModule);
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
}
else {
// like /** @module foo */ exports = {bar: 1};
// or /** blah */ this.foo = 1;
memberof = parser.resolveThis(astNode);
scopePunc = (nameStartsWith === 'exports') ?
SCOPE_PUNC.STATIC :
SCOPE_PUNC.INSTANCE;
// like /** @module foo */ this.bar = 1;
// like the following at the top level of a module:
// this.foo = 1;
if (nameStartsWith === 'this' && currentModule && !memberof) {
memberof = currentModule;
memberof = currentModule.longname;
scopePunc = SCOPE_PUNC.STATIC;
}
else {
scopePunc = SCOPE_PUNC.INSTANCE;
}
}
return {
@ -180,18 +188,26 @@ function addSymbolMemberof(parser, doclet, astNode) {
var basename;
var memberof;
var memberofInfo;
var moduleOriginalName = '';
var resolveTargetRegExp;
var scopePunc;
var unresolved;
// TODO: is this the correct behavior, given that we don't always use the AST node?
if (!astNode) {
return;
}
// check to see if the doclet name is an unresolved reference to the module wrapper
unresolved = unresolvedName.exec(doclet.name);
// check to see if the doclet name is an unresolved reference to the module object, or to `this`
// TODO: handle cases where the module object is shadowed in the current scope
if (currentModule) {
moduleOriginalName = '|' + currentModule.originalName;
}
resolveTargetRegExp = new RegExp('^((?:module.)?exports|this' + moduleOriginalName +
')(\\.|$)');
unresolved = resolveTargetRegExp.exec(doclet.name);
if (unresolved) {
memberofInfo = findModuleMemberof(parser, doclet, astNode, unresolved[1]);
memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]);
memberof = memberofInfo.memberof;
scopePunc = memberofInfo.scopePunc;
@ -247,9 +263,9 @@ function newSymbolDoclet(parser, docletSrc, e) {
return false;
}
// set the scope to global unless a) the doclet is a memberof something or b) the current
// module exports only this symbol
if (!newDoclet.memberof && currentModule !== newDoclet.name) {
// set the scope to global unless a) the doclet is a memberof something or b) we're in a module
// that exports only this symbol
if ( !newDoclet.memberof && (!currentModule || currentModule.longname !== newDoclet.name) ) {
newDoclet.scope = 'global';
}

16
test/fixtures/exportstag7.js vendored Normal file
View File

@ -0,0 +1,16 @@
'use strict';
/** @exports my/shirt */
var myShirt = exports;
/** A property of the module. */
myShirt.color = 'black';
/** @constructor */
myShirt.Turtleneck = function(size) {
/** A property of the class. */
this.size = size;
};
/** Iron the turtleneck. */
myShirt.Turtleneck.prototype.iron = function() {};

View File

@ -133,4 +133,43 @@ describe('@exports tag', function() {
expect(size.memberof).toBe('module:my/shirt.Turtleneck');
});
});
describe("alias to the 'exports' object", function() {
var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag7.js');
var shirt = docSet.getByLongname('module:my/shirt')[0];
var color = docSet.getByLongname('module:my/shirt.color')[0];
var tneck = docSet.getByLongname('module:my/shirt.Turtleneck')[0];
var size = docSet.getByLongname('module:my/shirt.Turtleneck#size')[0];
var iron = docSet.getByLongname('module:my/shirt.Turtleneck#iron')[0];
it('When a symbol has an @exports tag, the doclet is aliased to "module:" + the tag value.', function() {
expect(typeof shirt).toBe('object');
expect(shirt.alias).toBe('my/shirt');
expect(shirt.undocumented).not.toBeDefined();
});
it('When a symbol has an @exports tag, the doclet kind is set to module.', function() {
expect(shirt.kind).toEqual('module');
});
it('When a symbol tagged with @exports is an alias to "exports", the symbol properties are documented as members of the module.', function() {
expect(typeof color).toBe('object');
expect(color.memberof).toBe('module:my/shirt');
expect(typeof tneck).toBe('object');
expect(tneck.memberof).toBe('module:my/shirt');
});
it('When a symbol tagged with @exports is an alias to "exports", and a symbol property contains a class, the instance members of the class are documented correctly.', function() {
expect(typeof size).toBe('object');
expect(size.name).toBe('size');
expect(size.memberof).toBe('module:my/shirt.Turtleneck');
expect(size.scope).toBe('instance');
expect(typeof iron).toBe('object');
expect(iron.name).toBe('iron');
expect(iron.memberof).toBe('module:my/shirt.Turtleneck');
expect(iron.scope).toBe('instance');
});
});
});