From 3220279b3057a237ac9f87f80fd7737c782a790f Mon Sep 17 00:00:00 2001 From: Michael Mathews Date: Sun, 11 Jul 2010 22:51:30 +0100 Subject: [PATCH] Much work to get correct namepaths calculated when user doesn`t provide @name or @isa. --- modules/jsdoc/doclet.js | 16 ++- modules/jsdoc/name.js | 76 ++++++++---- modules/jsdoc/parser.js | 105 ++++++++++++---- modules/jsdoc/tagdictionary.js | 4 +- test/runall.js | 5 +- test/samples/jsdoc_infermethod.js | 42 +++++++ test/samples/jsdoc_resolvefunc.js | 51 ++++++++ test/samples/jsdoc_resolvevar.js | 39 ++++++ test/samples/jsdoc_test.js | 1 - test/tests/07_jsdoc_resolvefunc.js | 113 ++++++++++++++++++ test/tests/07_jsdoc_resolvevar.js | 17 +++ .../{07_tag_param.js => 19_tag_param.js} | 0 12 files changed, 416 insertions(+), 53 deletions(-) create mode 100644 test/samples/jsdoc_infermethod.js create mode 100644 test/samples/jsdoc_resolvefunc.js create mode 100644 test/samples/jsdoc_resolvevar.js create mode 100644 test/tests/07_jsdoc_resolvefunc.js create mode 100644 test/tests/07_jsdoc_resolvevar.js rename test/tests/{07_tag_param.js => 19_tag_param.js} (100%) diff --git a/modules/jsdoc/doclet.js b/modules/jsdoc/doclet.js index 6ef99558..2ce5fdaf 100644 --- a/modules/jsdoc/doclet.js +++ b/modules/jsdoc/doclet.js @@ -50,7 +50,7 @@ postprocess(doclet); name.resolve(doclet); - + return doclet } @@ -214,6 +214,20 @@ return o; } + Doclet.prototype.isInner = function() { + var access = this.tagValue('access'); + + if (!access) { + return false; + } + else if (typeof access === 'string') { + return (access === 'inner'); + } + else { + return (access.indexOf('inner') > -1); + } + } + /** Remove JsDoc comment slash-stars. Trims white space. @private diff --git a/modules/jsdoc/name.js b/modules/jsdoc/name.js index a9ea5680..d3f01ddb 100644 --- a/modules/jsdoc/name.js +++ b/modules/jsdoc/name.js @@ -31,8 +31,6 @@ path, shortname, prefix; - //, - //supportedNamespaces = ['module', 'event', 'file']; // only keep the first word of the first tagged name name = name.split(/\s+/g)[0]; @@ -121,34 +119,44 @@ Resolve how to document the `this.` portion of a symbol name. */ exports.resolveThis = function(name, node, doclet) { + var enclosing, enclosingDoc, memberof = (doclet.tagValue('memberof') || '').replace(/\.prototype\.?/g, '#'); if (node.parent && node.parent.type === Token.OBJECTLIT) { if (enclosing = node.parent) { - enclosingDoc = exports.docFromNode(enclosing) || {}; - memberof = (enclosingDoc.tagValue('path') || '').replace(/\.prototype\.?/g, '#'); - - if (!memberof) { - memberof = enclosingDoc.path; - memberof = memberof || '[[anonymousObject]]'; - } - - if (memberof) { - name = memberof + (memberof[memberof.length-1] === '#'?'':'.') + name; + enclosingDoc = exports.docFromNode(enclosing); + if (enclosingDoc) { + memberof = (enclosingDoc.tagValue('path') || '').replace(/\.prototype\.?/g, '#'); + + if (!memberof) { + memberof = enclosingDoc.path; + memberof = memberof || '[[anonymousObject]]'; + } + + if (memberof) { + name = memberof + (memberof[memberof.length-1] === '#'?'':'.') + name; + } } } } - else if (name.indexOf('this.') === 0) { // assume `this` refers to innermost constructor + else if (name.indexOf('this.') === 0) { if (!memberof || memberof === 'this') { enclosing = node.getEnclosingFunction() enclosingDoc = exports.docFromNode(enclosing); - memberof = enclosingDoc? enclosingDoc.tagValue('path') : ''; - + + if (enclosingDoc) { + if (enclosingDoc.isInner()) memberof = ''; // inner functions have `this` scope of global + else memberof = enclosingDoc.tagValue('path'); + } + else { + memberof = ''; + } + if (enclosing && !memberof) { - memberof = ''; //[[anonymousFunction]] + memberof = ''; // [[anonymousFunction]] name = name.slice(5); // remove `this.` } else if (!enclosing) { @@ -156,22 +164,44 @@ } if (memberof || !enclosing) { - // `this` refers to nearest instance in the name path + // `this` refers to nearest non-inner member in the name path if (enclosingDoc && enclosingDoc.tagValue('isa') !== 'constructor') { - var parts = memberof.split('#'); - parts.pop(); - memberof = parts.join('#'); + var parts = memberof.split(/[#~.]/); + var suffix = parts.pop(); + memberof = memberof.slice(0, -suffix.length); // remove suffix from memberof } - name = memberof + (memberof? '#':'') + name.slice(5); // replace `this.` with memberof + var joiner = (memberof === '')? '' : (/[#~.]$/.test(memberof))? '' : '#'; + name = memberof + joiner + name.slice(5); // replace `this.` with memberof } } else { - name = name.slice(5); + name = name.slice(5); // this means global? } } return name; } + + var G = this; + /** + Resolve how to document the name of an inner symbol. + */ + exports.resolveInner = function(name, node, doclet) { + var enclosing = node.getEnclosingFunction(), + enclosingDoc = exports.docFromNode(enclosing); + + if (enclosingDoc) { + memberof = enclosingDoc.tagValue('path'); + } + else { + memberof = (enclosing && enclosing.name == '')? '[[anonymous]]' : ''; + } + if (memberof) { + name = memberof + '~' + name; + } + + return name; + } /** Keep track of anonymous functions that have been assigned to documented symbols. @@ -187,7 +217,7 @@ return exports.refs[i][1]; } } - + return null; } // tuples, like [ [noderef, doclet], [noderef, doclet] ] diff --git a/modules/jsdoc/parser.js b/modules/jsdoc/parser.js index e470617a..33831390 100644 --- a/modules/jsdoc/parser.js +++ b/modules/jsdoc/parser.js @@ -34,23 +34,35 @@ } // like function foo() {} - if (node.type == Token.FUNCTION) { + if (node.type == Token.FUNCTION && String(node.name) !== '') { + commentSrc = (node.jsDoc)? String(node.jsDoc) : ''; - if (node.jsDoc) { - commentSrc = '' + node.jsDoc; + if (commentSrc) { + thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); + thisDocletName = thisDoclet.tagValue('path'); - if (commentSrc) { - thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); - thisDocletName = thisDoclet.tagValue('path'); - - if (thisDoclet.hasTag('isa') && !thisDocletName) { - thisDoclet.setName('' + node.name); - - doclets.addDoclet(thisDoclet); - } - - name.refs.push([node, thisDoclet]); + if (!thisDoclet.hasTag('isa')) { // guess isa from the source code + thisDoclet.addTag('isa', 'method') } + + if (!thisDocletName) { // guess name from the source code + thisDocletName = name.resolveInner(node.name, node, thisDoclet); + thisDoclet.setName(thisDocletName); + + doclets.addDoclet(thisDoclet); + } + name.refs.push([node, thisDoclet]); + } + else { // an uncommented function? + // this thing may have commented members, so keep a ref to the thing but don't add it to the doclets list + thisDoclet = doclet.makeDoclet('[[undocumented]]', node, currentSourceName); + + nodeName = name.resolveThis(node.name, node, thisDoclet); + thisDoclet.setName(nodeName); + name.refs.push([ + node, + thisDoclet + ]); } return true; @@ -58,50 +70,93 @@ // like foo = function(){} or foo: function(){} if (node.type === Token.ASSIGN || node.type === Token.COLON) { + var nodeName = nodeToString(node.left), nodeKind = ''; commentSrc = node.jsDoc || node.left.jsDoc; if (commentSrc) { commentSrc = '' + commentSrc; - thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); thisDocletName = thisDoclet.tagValue('name'); nodeKind = thisDoclet.tagValue('isa'); - if (thisDoclet.hasTag('isa') && !thisDocletName) { - nodeName = name.resolveThis( nodeName, node, thisDoclet ); + if (!thisDoclet.hasTag('isa')) { // guess isa from the source code + if (node.right.type == Token.FUNCTION) { // assume it's a method + thisDoclet.addTag('isa', 'method'); + } + else { + thisDoclet.addTag('isa', 'property'); + } + } + + if (!thisDocletName) { // guess name from the source code + nodeName = name.resolveThis(nodeName, node, thisDoclet); + thisDoclet.setName(nodeName); doclets.addDoclet(thisDoclet); } name.refs.push([node.right, thisDoclet]); } - + else { // an uncommented objlit or anonymous function? + + // this thing may have commented members, so keep a ref to the thing but don't add it to the doclets list + thisDoclet = doclet.makeDoclet('[[undocumented]]', node, currentSourceName); + nodeName = name.resolveThis(nodeName, node, thisDoclet); + + thisDoclet.setName(nodeName); + name.refs.push([ + node.right, + thisDoclet + ]); + } return true; } // like var foo = function(){} or var bar = {} - if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) { + if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) { var counter = 0, nodeKind; - + if (node.variables) for each (var n in node.variables.toArray()) { - if (n.target.type === Token.NAME && n.initializer) { + if (n.target.type === Token.NAME) { + var val = n.initializer; + commentSrc = (counter++ === 0 && !n.jsDoc)? node.jsDoc : n.jsDoc; if (commentSrc) { thisDoclet = doclet.makeDoclet('' + commentSrc, node, currentSourceName); thisDocletName = thisDoclet.tagValue('path'); nodeKind = thisDoclet.tagValue('isa'); - - if (thisDoclet.hasTag('isa') && !thisDocletName ) { - thisDocletName = n.target.string; + if (!thisDoclet.hasTag('isa') && val) { // guess isa from the source code + if (val.type == Token.FUNCTION) { + thisDoclet.addTag('isa', 'method'); + } + else { + thisDoclet.addTag('isa', 'property'); + } + } + + if (!thisDocletName ) { // guess name from the source code + //thisDocletName = n.target.string; + thisDocletName = name.resolveInner(n.target.string, node, thisDoclet); thisDoclet.setName(thisDocletName); doclets.addDoclet(thisDoclet); } + + if (val) name.refs.push([val, thisDoclet]); + } + else { // an uncommented objlit or anonymous function? + var nodeName = nodeToString(n.target); + // this thing may have commented members, so keep a ref to the thing but don't add it to the doclets list + thisDoclet = doclet.makeDoclet('[[undocumented]]', n.target, currentSourceName); + nodeName = name.resolveInner(nodeName, n.target, thisDoclet); + thisDoclet.setName(nodeName); + + if (val) name.refs.push([val, thisDoclet]); } } - name.refs.push([n.initializer, thisDoclet]); + } return true; } diff --git a/modules/jsdoc/tagdictionary.js b/modules/jsdoc/tagdictionary.js index 18bab0ea..b0cb3d71 100644 --- a/modules/jsdoc/tagdictionary.js +++ b/modules/jsdoc/tagdictionary.js @@ -24,7 +24,9 @@ 'augments': 'extends', 'throws': 'exception', 'class': 'classdesc', - 'this': 'thisobj' + 'this': 'thisobj', + 'preserve': 'ignore', + 'license': 'ignore' }; TagDictionary.resolveSynonyms = function(name) { diff --git a/test/runall.js b/test/runall.js index 4390a06e..e99e29dd 100644 --- a/test/runall.js +++ b/test/runall.js @@ -4,7 +4,8 @@ load(BASEDIR + '/test/tests/03_jsdoc_parser.js'); load(BASEDIR + '/test/tests/04_jsdoc_docset.js'); load(BASEDIR + '/test/tests/05_jsdoc_doclet.js'); load(BASEDIR + '/test/tests/06_jsdoc_tag.js'); -load(BASEDIR + '/test/tests/07_tag_param.js'); +load(BASEDIR + '/test/tests/07_jsdoc_resolvefunc.js'); +load(BASEDIR + '/test/tests/07_jsdoc_resolvevar.js'); load(BASEDIR + '/test/tests/08_tag_name.js'); load(BASEDIR + '/test/tests/09_tag_desc.js'); load(BASEDIR + '/test/tests/10_tag_constructor.js'); @@ -16,7 +17,7 @@ load(BASEDIR + '/test/tests/15_tag_type.js'); load(BASEDIR + '/test/tests/16_tag_return.js'); load(BASEDIR + '/test/tests/17_tag_example.js'); load(BASEDIR + '/test/tests/18_tag_class.js'); - +load(BASEDIR + '/test/tests/19_tag_param.js'); load(BASEDIR + '/test/tests/20_tag_file.js'); load(BASEDIR + '/test/tests/21_tag_const.js'); load(BASEDIR + '/test/tests/22_tag_preserve.js'); diff --git a/test/samples/jsdoc_infermethod.js b/test/samples/jsdoc_infermethod.js new file mode 100644 index 00000000..5786311d --- /dev/null +++ b/test/samples/jsdoc_infermethod.js @@ -0,0 +1,42 @@ +// /** A function that does stuff. */ +// function doStuff() { +// } +// +// /** +// * Shape is an abstract base class. It is defined simply +// * to have something to inherit from for geometric +// * subclasses +// * @constructor +// */ +// function Shape(color){ +// this.color = color; +// } +// +// /** +// * Get the name of the color for this shape +// * @returns A color string for this shape +// */ +// Shape.prototype.getColor = function() { +// return this.color; +// } +// +// var x, +// /** @return {number} */ +// getx = function(){}, +// y; +// + +ShapeFactory.prototype = { + util: { + /** + * Creates a new {@link Shape} instance. + * @return A new {@link Shape} + * @type Shape + */ + createShape: function() { + /** Track the most recent shape created. */ + this.lastShape = new Shape(); + return this.lastShape; + } + } + } \ No newline at end of file diff --git a/test/samples/jsdoc_resolvefunc.js b/test/samples/jsdoc_resolvefunc.js new file mode 100644 index 00000000..0fa5c609 --- /dev/null +++ b/test/samples/jsdoc_resolvefunc.js @@ -0,0 +1,51 @@ +// undocumented +ShapeFactory.prototype = { + // undocumented + util: { + // resolves to: @method ShapeFactory#util.createShape + /** + * Creates a new {@link Shape} instance. + * @return A new {@link Shape} + * @type Shape + */ + createShape: function() { + // resolves to: @property ShapeFactory#util.lastShape + /** Track the most recent shape created. */ + this.lastShape = new Shape(); + + return this.lastShape; + } + } +} + +// undocumented +foo = function() { + // resolves to: @property g + /** @type {number} */ + this.g = 1; +} + +/** @constructor */ +Foo = function() { + // resolves to: @method Foo#bar + /** two bar */ + this.bar = function(){}; + + // resolves to: @method Foo~inner + /** an inner function */ + function inner() { + + // resolves to: @method Foo~inner~deep + /** an nested inner function */ + function deep() { + // resolves to: @property globalProp + /** set a property */ + this.globalProp = 1; + } + } +} + +// resolves to: @method globalFunction +/** a global function */ +this.globalFunc = function() { +} \ No newline at end of file diff --git a/test/samples/jsdoc_resolvevar.js b/test/samples/jsdoc_resolvevar.js new file mode 100644 index 00000000..89942da9 --- /dev/null +++ b/test/samples/jsdoc_resolvevar.js @@ -0,0 +1,39 @@ + +// undocumented +this.globalprop = { + /** a child property */ + child1: { + /** a nested child func */ + child2: {} + } +}; + +(function ($) { + var io = { + /** @property */ + ip: function(){} + } +})(mylib); + +var go = { + /** @variable */ + gp: true +}; + +/** @variable */ +var foo, + /** @variable */ + bar; + + +// undocumented +function globalFunc() { + /** an inner property */ + var innerProp = 1; + + // an inner function */ + var innerFunc = function() { + /** a nested child func */ + var nestedProp = 1; + } +} diff --git a/test/samples/jsdoc_test.js b/test/samples/jsdoc_test.js index fc5f62f9..fd3932a5 100644 --- a/test/samples/jsdoc_test.js +++ b/test/samples/jsdoc_test.js @@ -37,7 +37,6 @@ function Shape(){ /** * This is an inner method, just used here as an example - * @method Shape~addReference * @private * @since version 0.5 * @author Sue Smart diff --git a/test/tests/07_jsdoc_resolvefunc.js b/test/tests/07_jsdoc_resolvefunc.js new file mode 100644 index 00000000..50a6a01f --- /dev/null +++ b/test/tests/07_jsdoc_resolvefunc.js @@ -0,0 +1,113 @@ +(function() { + var jsdoc, + doclets; + + JSpec.describe('jsdoc/name:resolvefunc', function() { + + before(function() { + // docsets can only be created by parsers + jsdoc = { + tag: require('jsdoc/tag'), + parser: require('jsdoc/parser') + }; + jsdoc.parser.parseFiles(BASEDIR + 'test/samples/jsdoc_resolvefunc.js'); + doclets = jsdoc.parser.result.map(function($){ return $.toObject(); }); + }); + + describe('A doclet that has no @name, that is a property of a nested, undocumented objlit', function() { + it('should be given a path that includes membership in the enclosing object', function() { + var doclet = doclets[0]; + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'createShape'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'ShapeFactory#util.createShape'); + }); + }); + + describe('A doclet that has no @name whose name starts with `this.`, that is a property of a nested objlit', function() { + it('should correctly resolve `this` to the nearest enclosing object', function() { + var doclet = doclets[1]; + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'lastShape'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'ShapeFactory#util.lastShape'); + }); + }); + + describe('A property doclet that has no @name or @isa whose name starts with `this.`, that is scoped to a non-constructor global function', function() { + it('should correctly resolve to a property of the global scope', function() { + var doclet = doclets[2]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'property'); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'g'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'g'); + }); + }); + + describe('A method doclet that has no @name or @isa whose name starts with `this.`, that is scoped to a constructor function', function() { + it('should correctly resolve to a method of the constructor', function() { + var doclet = doclets[4]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'method'); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'bar'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'Foo#bar'); + }); + }); + + describe('An inner method doclet that has no @name or @isa, and is scoped to a constructor function', function() { + it('should correctly resolve to an inner method of the constructor', function() { + var doclet = doclets[5]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'method'); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'inner'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'Foo~inner'); + expect(doclet).to(have_property, 'access'); + expect(doclet.access).to(eql, 'inner'); + }); + }); + + describe('A nested inner method doclet that has no @name or @isa, and is scoped to an inner method', function() { + it('should correctly resolve to an inner method of the a inner method', function() { + var doclet = doclets[6]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'method'); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'deep'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'Foo~inner~deep'); + expect(doclet).to(have_property, 'access'); + expect(doclet.access).to(eql, 'inner'); + }); + }); + + describe('A property doclet whose name starts with `this.`, that has no @name or @isa, and is scoped to an inner method', function() { + it('should correctly resolve to a property of the global scope', function() { + var doclet = doclets[7]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'property'); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'globalProp'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'globalProp'); + }); + }); + + describe('A global method whose name starts with `this.`, that has no @name or @isa', function() { + it('should correctly resolve to a property of the global scope', function() { + var doclet = doclets[8]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'method'); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'globalFunc'); + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'globalFunc'); + }); + }); + }); +})(); diff --git a/test/tests/07_jsdoc_resolvevar.js b/test/tests/07_jsdoc_resolvevar.js new file mode 100644 index 00000000..d373c74f --- /dev/null +++ b/test/tests/07_jsdoc_resolvevar.js @@ -0,0 +1,17 @@ +(function() { + var jsdoc, + doclets; + + JSpec.describe('jsdoc/name:resolvevar', function() { + + before(function() { + // docsets can only be created by parsers + jsdoc = { + tag: require('jsdoc/tag'), + parser: require('jsdoc/parser') + }; + jsdoc.parser.parseFiles(BASEDIR + 'test/samples/jsdoc_resolvevar.js'); + doclets = jsdoc.parser.result.map(function($){ return $.toObject(); }); + }); + }); +})(); diff --git a/test/tests/07_tag_param.js b/test/tests/19_tag_param.js similarity index 100% rename from test/tests/07_tag_param.js rename to test/tests/19_tag_param.js