From 22cb3d2ef6df337e75d0fb7608217e7053958b4f Mon Sep 17 00:00:00 2001 From: Michael Mathews Date: Sun, 4 Jul 2010 01:48:04 +0100 Subject: [PATCH] Added tests for @file, @returns, and @type. --- modules/jsdoc/doclet.js | 139 +++++++++++++++++-------------- modules/jsdoc/docset.js | 2 +- modules/jsdoc/name.js | 49 +++++++---- modules/jsdoc/parser.js | 55 ++++++------ modules/jsdoc/schema.js | 130 ++++++++++++++++++++++++++--- modules/jsdoc/tag.js | 112 +++++++++++++++---------- modules/jsdoc/tagdictionary.js | 88 +++++++++++++++++++ modules/jsdoc/type.js | 24 +++--- test/runall.js | 4 + test/samples/tag_constructor.js | 18 ++++ test/samples/tag_file_1.js | 21 +++++ test/tests/05_jsdoc_doclet.js | 14 ++-- test/tests/06_jsdoc_tag.js | 11 ++- test/tests/10_tag_constructor.js | 108 ++++++++++-------------- test/tests/14_tag_member.js | 31 +++++-- test/tests/15_tag_type.js | 67 +++++++++++++++ test/tests/16_tag_return.js | 69 +++++++++++++++ test/tests/20_tag_file.js | 32 +++++++ 18 files changed, 713 insertions(+), 261 deletions(-) create mode 100644 modules/jsdoc/tagdictionary.js create mode 100644 test/samples/tag_constructor.js create mode 100644 test/samples/tag_file_1.js create mode 100644 test/tests/15_tag_type.js create mode 100644 test/tests/16_tag_return.js create mode 100644 test/tests/20_tag_file.js diff --git a/modules/jsdoc/doclet.js b/modules/jsdoc/doclet.js index 8d78d5d4..da432c05 100644 --- a/modules/jsdoc/doclet.js +++ b/modules/jsdoc/doclet.js @@ -33,7 +33,7 @@ tags = parse_tag.parse(commentSrc); try { - preprocess(tags); + preprocess(tags, meta); } catch(e) { e.message = 'Cannot make doclet from JSDoc comment found at '+ meta.file + ' ' + meta.line @@ -45,7 +45,7 @@ doclet = new Doclet(tags); doclet.meta = meta; - + postprocess(doclet); name.resolve(doclet); @@ -73,33 +73,44 @@ @param {string name */ Doclet.prototype.setName = function(nameToSet) { - this.tagText('name', nameToSet); + this.setTag('name', nameToSet); nameToSet = name.resolve(this); } /** - Return the text of the last tag with the given name. - @method Doclet#tagText + Return the value of the last tag with the given name. + @method Doclet#tagValue @param {String} tagName - @returns {String} The text of the found tag. + @returns {*} The value of the found tag. */ - Doclet.prototype.tagText = function(tagName, text) { - var i = this.tags.length; - while(i--) { + Doclet.prototype.tagValue = function(tagName) { + for (var i = 0, leni = this.tags.length; i < leni; i++) { if (this.tags[i].name === tagName) { - if (text) { this.tags[i].text = text; } - return this.tags[i].text; + return this.tags[i].value; } } - // still here? - if (text) { - this.tags.push( parse_tag.fromTagText(tagName + ' ' + text) ); - return text; + return null; + } + + + /** + Return the value of the last tag with the given name. + @method Doclet#setTag + @param {String} tagName + @returns {*} The value of the found tag. + */ + Doclet.prototype.setTag = function(tagName, tagValue) { + + for (var i = 0, leni = this.tags.length; i < leni; i++) { + if (this.tags[i].name === tagName) { + this.tags[i].value = tagValue; + return ; + } } - - return ''; + + this.tags[this.tags.length] = parse_tag.fromText(tagName + ' ' + tagValue); } /** @@ -154,10 +165,10 @@ // tag value is not an object, it's just a simple string if (!tag.pname && !tag.pdesc && !(tag.type && tag.type.length)) { // TODO: should check the list instead? if (flavor === 'xml' && tagName === 'example') { - tagValue['#cdata'] = tag.text; // TODO this is only meaningful to XML, move to a tag.format(style) method? + tagValue['#cdata'] = tag.value; // TODO this is only meaningful to XML, move to a tag.format(style) method? } else { - tagValue = tag.text; + tagValue = tag.value; } } @@ -176,6 +187,7 @@ o.meta = this.meta; } + return o; } @@ -228,48 +240,49 @@ @param {Array.} tags @returns undefined */ - function preprocess(tags) { + function preprocess(tags, meta) { var name = '', taggedName = '', isa = '', - taggedDenom = '', + taggedIsa = '', memberof = '', - taggedMemberof = ''; + taggedMemberof = '', + isFile = false; for (var i = 0, leni = tags.length; i < leni; i++) { if (tags[i].name === 'private') { - tags[tags.length] = parse_tag.fromTagText('access private'); + tags[tags.length] = parse_tag.fromText('access private'); } else if (tags[i].name === 'protected') { - tags[tags.length] = parse_tag.fromTagText('access protected'); + tags[tags.length] = parse_tag.fromText('access protected'); } else if (tags[i].name === 'public') { - tags[tags.length] = parse_tag.fromTagText('access public'); + tags[tags.length] = parse_tag.fromText('access public'); } else if (tags[i].name === 'const') { - tags[tags.length] = parse_tag.fromTagText('attribute constant'); + tags[tags.length] = parse_tag.fromText('attribute constant'); } else if (tags[i].name === 'readonly') { - tags[tags.length] = parse_tag.fromTagText('attribute readonly'); + tags[tags.length] = parse_tag.fromText('attribute readonly'); } else if (tags[i].name === 'name') { - if (name && name !== tags[i].text) { - throw new DocTagConflictError('Conflicting names in documentation: '+name+', '+tags[i].text); + if (name && name !== tags[i].value) { + throw new DocTagConflictError('Conflicting names in documentation: '+name+', '+tags[i].value); } - taggedName = name = tags[i].text; + taggedName = name = tags[i].value; } else if (tags[i].name === 'isa') { - if (isa && isa !== tags[i].text) { - throw new DocTagConflictError('Symbol has too many denominations, cannot be both: ' + isa + ' and ' + tags[i].text); + if (isa && isa !== tags[i].value) { + throw new DocTagConflictError('Symbol has too many denominations, cannot be both: ' + isa + ' and ' + tags[i].value); } - taggedDenom = isa = tags[i].text; + taggedIsa = isa = tags[i].value; } else if (tags[i].name === 'memberof') { if (memberof) { throw new DocTagConflictError('doclet has too many tags of type: @memberof.'); } - taggedMemberof = memberof = tags[i].text; + taggedMemberof = memberof = tags[i].value; } if ( nameables.indexOf(tags[i].name) > -1 ) { @@ -277,19 +290,15 @@ // for backwards compatability we ignore a @property in a doclet after a @constructor } else { - if (tags[i].text) { - if (name && name !== tags[i].text) { - throw new DocTagConflictError('Conflicting names in documentation: '+name+', '+tags[i].text); + if (tags[i].value) { + if (name && name !== tags[i].value) { + throw new DocTagConflictError('Conflicting names in documentation: '+name+', '+tags[i].value); } - name = tags[i].text; + name = tags[i].value; } if (tags[i].pdesc) { - tags[tags.length] = parse_tag.fromTagText('desc ' + tags[i].pdesc); - } - - if (tags[i].type) { - tags[tags.length] = parse_tag.fromTagText('type ' + tags[i].type.join('|')); + tags[tags.length] = parse_tag.fromText('desc ' + tags[i].pdesc); } if (isa && isa !== tags[i].name) { @@ -299,13 +308,17 @@ if (isa === 'const') { isa = 'property'; } // an exception to the namebale rule } } + else if (tags[i].name === 'file') { // the only isa which cannot have a name after @file + isFile = true; + isa = 'file'; + } if ( memberofs.hasOwnProperty(tags[i].name) ) { - if (tags[i].text) { + if (tags[i].value) { if (memberof) { throw new DocTagConflictError('doclet has too many tags of type: @memberof.'); } - memberof = tags[i].text; + memberof = tags[i].value; } if (isa && isa !== memberofs[tags[i].name]) { @@ -316,47 +329,51 @@ } if (name && !taggedName) { - tags[tags.length] = parse_tag.fromTagText('name ' + name); + tags[tags.length] = parse_tag.fromText('name ' + name); } - if (isa && !taggedDenom) { - tags[tags.length] = parse_tag.fromTagText('isa ' + isa); + if ( isFile && !(name || taggedName) ) { + tags[tags.length] = parse_tag.fromText('name file:'+meta.file+''); + } + + if (isa && !taggedIsa) { + tags[tags.length] = parse_tag.fromText('isa ' + isa); } if (memberof && !taggedMemberof) { - tags[tags.length] = parse_tag.fromTagText('memberof ' + memberof); + tags[tags.length] = parse_tag.fromText('memberof ' + memberof); } } function postprocess(doclet) { if ( doclet.hasTag('class') && !doclet.hasTag('constructor') ) { - doclet.tags[doclet.tags.length] = parse_tag.fromTagText('isa constructor'); + doclet.tags[doclet.tags.length] = parse_tag.fromText('isa constructor'); } - if ( doclet.hasTag('enum')) { - if (!doclet.hasTag('type')) { - doclet.tags[doclet.tags.length] = parse_tag.fromTagText('type number'); + if ( doclet.hasTag('enum') ) { + if ( !doclet.hasTag('type') ) { + doclet.tags[doclet.tags.length] = parse_tag.fromText('type number'); } - if (!doclet.hasTag('readonly') && !doclet.hasTag('const')) { - doclet.tags[doclet.tags.length] = parse_tag.fromTagText('attribute constant'); + if ( !doclet.hasTag('readonly') && !doclet.hasTag('const') ) { + doclet.tags[doclet.tags.length] = parse_tag.fromText('attribute constant'); } } - if ( doclet.hasTag('const')) { - if (!doclet.hasTag('isa')) { - doclet.tags[doclet.tags.length] = parse_tag.fromTagText('isa property'); + if ( doclet.hasTag('const') ) { + if ( !doclet.hasTag('isa') ) { + doclet.tags[doclet.tags.length] = parse_tag.fromText('isa property'); } if (!doclet.hasTag('readonly') && !doclet.hasTag('const')) { - doclet.tags[doclet.tags.length] = parse_tag.fromTagText('attribute constant'); + doclet.tags[doclet.tags.length] = parse_tag.fromText('attribute constant'); } } } function DocTagConflictError(message) { - this.name = "DocTagConflictError"; - this.message = (message || ""); + this.name = 'DocTagConflictError'; + this.message = (message || ''); } DocTagConflictError.prototype = Error.prototype; diff --git a/modules/jsdoc/docset.js b/modules/jsdoc/docset.js index 99e346a5..e7c9e81c 100644 --- a/modules/jsdoc/docset.js +++ b/modules/jsdoc/docset.js @@ -13,7 +13,7 @@ i = doclets.length; while (i--) { - if (doclets[i].tagText('path') === docName) { + if (doclets[i].tagValue('path') === docName) { foundDocs.unshift( doclets[i] ); } } diff --git a/modules/jsdoc/name.js b/modules/jsdoc/name.js index 58cba72d..fd7c2d87 100644 --- a/modules/jsdoc/name.js +++ b/modules/jsdoc/name.js @@ -23,15 +23,15 @@ @param {Doclet} doclet */ exports.resolve = function(doclet) { - var isa = doclet.tagText('isa'), + var isa = doclet.tagValue('isa'), ns = '', - name = doclet.tagText('name'), - memberof = doclet.tagText('memberof'), + name = doclet.tagValue('name') || '', + memberof = doclet.tagValue('memberof') || '', path, shortname, prefix, - supportedNamespaces = ['module', 'event']; - + supportedNamespaces = ['module', 'event', 'file']; + // only keep the first word of the first tagged name name = name.split(/\s+/g)[0]; @@ -42,9 +42,7 @@ name = name.replace(/\.prototype\.?/g, '#'); path = shortname = name; - - - + if (memberof) { // like @name foo.bar, @memberof foo if (name.indexOf(memberof) === 0) { @@ -52,9 +50,9 @@ [prefix, name] = exports.shorten(name); } } - else { + else if (isa !== 'file') { [memberof, name] = exports.shorten(name); - doclet.tagText('memberof', memberof); + if (memberof) { doclet.setTag('memberof', memberof); } } // if name doesn't already have a doc-namespace and needs one @@ -67,28 +65,43 @@ // add doc-namespace to path ns = isa + ':'; } - - doclet.tagText('name', name); + + if (name) doclet.setTag('name', name); if (memberof && name.indexOf(memberof) !== 0) { path = memberof + (/#$/.test(memberof)? '' : '.') + ns + name; } - if (path) { - doclet.tagText('path', path); + doclet.setTag('path', path); } return path; } exports.shorten = function(path) { + // quoted strings in a path are atomic + var atoms = [], + cursor = 0; + path = path.replace(/(".+?")/g, function($) { + var token = '@' + atoms.length + '@'; + atoms.push($); + return token; + }); + var shortname = path.split(/([#.-])/).pop(), splitOn = RegExp.$1, splitAt = path.lastIndexOf(splitOn), prefix = (splitOn && splitAt !== -1)? path.slice(0, splitAt) : ''; if (splitOn === '#') { prefix = prefix + splitOn; } + + // restore quoted strings back again + for (var i = 0, leni = atoms.length; i < leni; i++) { + prefix = prefix.replace('@'+i+'@', atoms[i]); + shortname = shortname.replace('@'+i+'@', atoms[i]); + } + return [prefix, shortname]; } @@ -98,12 +111,12 @@ exports.resolveThis = function(name, node, doclet) { var enclosing, enclosingDoc, - memberof = (doclet.tagText('memberof') || '').replace(/\.prototype\.?/g, '#'); + 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.tagText('path') || '').replace(/\.prototype\.?/g, '#'); + memberof = (enclosingDoc.tagValue('path') || '').replace(/\.prototype\.?/g, '#'); if (!memberof) { memberof = enclosingDoc.path; @@ -120,7 +133,7 @@ enclosing = node.getEnclosingFunction() enclosingDoc = exports.docFromNode(enclosing); - memberof = enclosingDoc? enclosingDoc.tagText('path') : ''; + memberof = enclosingDoc? enclosingDoc.tagValue('path') : ''; if (enclosing && !memberof) { memberof = ''; //[[anonymousFunction]] @@ -132,7 +145,7 @@ if (memberof || !enclosing) { // `this` refers to nearest instance in the name path - if (enclosingDoc && enclosingDoc.tagText('isa') !== 'constructor') { + if (enclosingDoc && enclosingDoc.tagValue('isa') !== 'constructor') { var parts = memberof.split('#'); parts.pop(); memberof = parts.join('#'); diff --git a/modules/jsdoc/parser.js b/modules/jsdoc/parser.js index 1f775142..74dcd1ca 100644 --- a/modules/jsdoc/parser.js +++ b/modules/jsdoc/parser.js @@ -12,37 +12,40 @@ var commentSrc = '', thisDoclet = null, thisDocletName = ''; - - // look for all comments that have names provided - if (node.type === Token.SCRIPT && node.comments) { - for each (var comment in node.comments.toArray()) { - if (comment.commentType === Token.CommentType.JSDOC) { - commentSrc = '' + comment.toSource(); - if (commentSrc) { - thisDoclet = doclet.makeDoclet(commentSrc, comment, currentSourceName); - if ( thisDoclet.hasTag('name') ) { - doclets.push(thisDoclet); - if (thisDoclet.tagText('isa') === 'module') { - name.setCurrentModule( thisDoclet.tagText('path') ); - } - } - } - } - } - } - + + // look for all comments that have names provided + if (node.type === Token.SCRIPT && node.comments) { + for each (var comment in node.comments.toArray()) { + if (comment.commentType === Token.CommentType.JSDOC) { + commentSrc = '' + comment.toSource(); + if (commentSrc) { + thisDoclet = doclet.makeDoclet(commentSrc, comment, currentSourceName); + + if ( thisDoclet.hasTag('name') ) { + doclets.push(thisDoclet); + if (thisDoclet.tagValue('isa') === 'module') { + name.setCurrentModule( thisDoclet.tagValue('path') ); + } + } + } + } + } + } + // like function foo() {} if (node.type == Token.FUNCTION) { + if (node.jsDoc) { commentSrc = '' + node.jsDoc; - + if (commentSrc) { thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); - thisDocletName = thisDoclet.tagText('path'); - + thisDocletName = thisDoclet.tagValue('path'); + if (!thisDocletName) { thisDoclet.setName('' + node.name); + doclets.push(thisDoclet); } @@ -63,8 +66,8 @@ commentSrc = '' + commentSrc; thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); - thisDocletName = thisDoclet.tagText('name'); - nodeKind = thisDoclet.tagText('isa'); + thisDocletName = thisDoclet.tagValue('name'); + nodeKind = thisDoclet.tagValue('isa'); if (!thisDocletName) { nodeName = name.resolveThis( nodeName, node, thisDoclet ); @@ -88,8 +91,8 @@ commentSrc = (counter++ === 0 && !n.jsDoc)? node.jsDoc : n.jsDoc; if (commentSrc) { thisDoclet = doclet.makeDoclet('' + commentSrc, node, currentSourceName); - thisDocletName = thisDoclet.tagText('path'); - nodeKind = thisDoclet.tagText('isa'); + thisDocletName = thisDoclet.tagValue('path'); + nodeKind = thisDoclet.tagValue('isa'); if ( !thisDocletName ) { thisDocletName = n.target.string; diff --git a/modules/jsdoc/schema.js b/modules/jsdoc/schema.js index 8c24b3fd..f5e20744 100644 --- a/modules/jsdoc/schema.js +++ b/modules/jsdoc/schema.js @@ -5,16 +5,22 @@ @see */ -var jsdoc = jsdoc || {}; -jsdoc.schema = (typeof exports === 'undefined')? {} : exports; // like commonjs - -jsdoc.schema.jsdocSchema = { +exports.jsdocSchema = { "properties": { - "doc": { + "docnode": { + "type": "array", "items": { "type": "object", "properties": { - "path": { + "id": { + "type": "string", + "maxItems": 1 + }, + "summary": { + "type": "string", + "maxItems": 1 + }, + "desc": { "type": "string", "maxItems": 1 }, @@ -30,30 +36,130 @@ jsdoc.schema.jsdocSchema = { "isa": { "type": "string", "maxItems": 1, - "enum": ["constructor", "module", "event", "namespace", "method", "member", "enum"] + "enum": ["constructor", "module", "event", "namespace", "method", "property", "enum", "class", "interface", "constant", "file"] }, + "access": { + "type": "string", + "maxItems": 1, + "enum": ["private", "protected", "public"] + }, + "type": { + "type": "array", + "optional": true, + "items": { + "type": "string" + } + }, + "param" : { + "type": "array", + "optional": true, + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "optional": true, + "items": { + "type": "string" + } + }, + "isoptional": { + "type": "boolean", + "optional": true, + "default": true + }, + "isnullable": { + "type": "boolean", + "optional": true, + "default": true + }, + "defaultvalue": { + "optional": true + }, + "name": { + "type": "string", + }, + "desc": { + "type": "string", + "optional": true + } + } + } + }, + + "meta": { + "type": "object", + "optional": true, + "maxItems": 1, "file": { "type": "string", "optional": true, "maxItems": 1 }, "line": { + "type": "number", + "optional": true, + "maxItems": 1 + }, + "category": { "type": "string", "optional": true, "maxItems": 1 }, - "optional": true, - "maxItems": 1 + "tags": { + "type": "array", + "optional": true, + "items": { + "type": "object", + "properties": { + "tagname": { + "type": "string" + }, + "tagtext": { + "type": "string", + "optional": true + } + } + } + } } } } }, "meta": { + "type": "object", "optional": true, - "date": { - "type": "string", - "maxItems": 1 + "maxItems": 1, + "project": { + "type": "object", + "optional": true, + "maxItems": 1, + "name": { + "type": "string", + "maxItems": 1 + }, + "uri": { + "type": "string", + "maxItems": 1, + "format": "uri" + } + }, + "generated": { + "type": "object", + "optional": true, + "maxItems": 1, + "date": { + "type": "string", + "maxItems": 1, + "optional": true, + "format": "date-time" + }, + "parser": { + "type": "string", + "maxItems": 1, + "optional": true + } } } } diff --git a/modules/jsdoc/tag.js b/modules/jsdoc/tag.js index 08f4cc17..23154341 100644 --- a/modules/jsdoc/tag.js +++ b/modules/jsdoc/tag.js @@ -9,73 +9,95 @@ @module jsdoc/tag */ (function() { - var jsdoc_type = require('jsdoc/type'); + var jsdoc_type = require('jsdoc/type'), + tagz = require('jsdoc/tagdictionary').TagDictionary; - exports.fromTagText = function(tagText) { - return new Tag(tagText); + exports.fromText = function(tagText) { + var tag = new Tag(tagText); + return tag; } // tags that have {type} (name desc|text) - var longTags = ['param', 'constructor', 'const', 'module', 'event', 'namespace', 'method', 'member', 'function', 'variable', 'enum', 'returns']; + var longTags = ['param', 'constructor', 'type', 'const', 'module', 'event', 'namespace', 'method', 'member', 'function', 'variable', 'enum', 'returns']; // tags that have {type} text var anonTags = ['returns']; /** @private - @constructor Tag + @constructor module:jsdoc/tag.Tag @param {string} tagText */ function Tag(tagText) { + /** @property {string} - The raw text of this tag, include everything after the @. */ this.raw = tagText; + + /** @property {string} - The name of this tag, the word adjacent to the @. */ this.name = ''; + + /** @property {Array} - Zero or more type specifiers. */ this.type = []; - this.text = ''; + + /** @property {*} - The value of this tag. */ + this.value = null; + + /** @property {string} - If this is a long tag, then this will be the parameter name. */ this.pname = ''; + + /** @property {string} - If this is a long tag, then this will be the parameter description. */ this.pdesc = ''; - // tagText is like: "tagname tag text" - var bits = tagText.match(/^\s*(\S+)(?:\s([\s\S]*))?$/); - - if (bits) { - this.name = (bits[1] || '').toLowerCase(); // like @name - this.name = trim( resolveSynonyms(this.name) ); - - this.text = bits[2] || ''; // all the rest of the tag + // raw is like: "tagname andsometagtext" + var parts = this.raw.match(/^\s*(\S+)(?:\s+([\s\S]*))?$/); - if (this.name !== 'example') { // example is the only tag that preserves whitespace - this.text = trim( this.text ); + if (parts) { + this.name = (parts[1] || '').toLowerCase(); // like @name + this.name = resolveSynonyms(this.name); + + tagText = parts[2] || ''; // all the rest of the tag + + if (tagz.lookUp(this.name).keepsWhitespace) { + this.value = tagText; + } + else { + this.value = trim(tagText); } if (longTags.indexOf(this.name) > -1) { // is a tag that uses the long format + var /*Array.*/ type, - /*string*/ text, + /*any*/ value, /*?boolean*/ optional, /*?boolean*/ nullable; - [type, text, optional, nullable] = jsdoc_type.parse(this.text); - // @type tags are the only tag that is not allowed to have a {type}! - if (this.name === 'type') { - text = text || type.join('|'); - type = []; - } - + [type, value, optional, nullable] = jsdoc_type.parse(this.value); + // don't add an empty type or null attributes if (type && type.length) { this.type = type; } + + // @type tags are special: the only tag that is not allowed to have a {type} + // their type becomes their value + if (this.name === 'type') { + value = (this.type[0] === '')? this.value.split(/\s*\|\s*/g) : this.type; + if (value.length === 1) value = value[0]; // single values don't need to be arrays + this.type = []; + } + if (optional !== null) { this.poptional = optional; } if (nullable !== null) { this.pnullable = nullable; } - this.text = text; - - if (anonTags.indexOf(this.name) > -1) { - this.pdesc = this.text; +// TODO protect @example from being overwritten? + this.value = value; + if (tagz.lookUp(this.name).canHavePname && tagz.lookUp(this.name).canHavePdesc) { // some tags just have {type} desc + if (typeof this.value === 'string') { + var [pname, pdesc, poptional, pdefault] = parsePname(this.value); + this.pname = pname; + this.pdesc = pdesc; + if (typeof poptional !== 'undefined') this.poptional = poptional; + this.pdefault = pdefault; + } } - else { - - var [pname, pdesc, poptional, pdefault] = parsePname(this.text); - this.pname = pname; - this.pdesc = pdesc; - if (typeof poptional !== 'undefined') this.poptional = poptional; - this.pdefault = pdefault; + else if (tagz.lookUp(this.name).canHavePdesc) { + this.pdesc = this.value; } } } @@ -85,8 +107,8 @@ Given the source of a jsdoc comment, finds the tags. @private @function parse - @param {string} commentSrc Unwrapped. - @returns Array. + @param {string} commentSrc Unwrapped raw source of the doc comment. + @returns {Array.} */ exports.parse = function(commentSrc) { var tags = []; @@ -96,7 +118,7 @@ .replace(/^(\s*)@(\S)/gm, '$1\\@$2') // replace splitter ats with an arbitrary sequence (unicode_recordseperator+@) .split('\\@') // then split on that arbitrary sequence .forEach(function($) { - var newTag = exports.fromTagText($); + var newTag = exports.fromText($); if (newTag.name) { tags.push(newTag); } }); @@ -109,19 +131,20 @@ } /** - Split the parameter name and parameter desc from the tag text. + Parse the parameter name and parameter desc from the tag text. @private @method parsePname @param {string} tagText - @returns Array. The pname and the pdesc. + @returns {Array.} [pname, pdesc, poptional, pdefault]. */ function parsePname(tagText) { var pname, pdesc, poptional, pdefault; - tagText.match(/^(\[[^\]]+\]|\S+)(\s+(\S[\s\S]*))?$/); + // like: pname, pname pdesc, or name - pdesc + tagText.match(/^(\[[^\]]+\]|\S+)((?:\s*\-\s*|\s+)(\S[\s\S]*))?$/); pname = RegExp.$1; pdesc = RegExp.$3; - + if ( /^\[\s*(.+?)\s*\]$/.test(pname) ) { pname = RegExp.$1; poptional = true; @@ -143,11 +166,14 @@ } } exports.synonyms = { + /*synonym*/ /*canonical*/ 'description': 'desc', 'function': 'method', 'variable': 'property', 'return': 'returns', - 'member': 'memberof' + 'member': 'memberof', + 'overview': 'file', + 'fileoverview':'file' } //TODO: move into a shared module? diff --git a/modules/jsdoc/tagdictionary.js b/modules/jsdoc/tagdictionary.js new file mode 100644 index 00000000..6b9b9872 --- /dev/null +++ b/modules/jsdoc/tagdictionary.js @@ -0,0 +1,88 @@ +/** + @overview Provides information about the various differnt types of tags. + */ + +(function() { + /** */ + exports.TagDictionary = {}; + exports.TagDictionary.lookUp = function(tagTitle) { + return this['@'+tagTitle] || {}; + } + exports.TagDictionary.synonyms = { + }; + + /** */ + function TagDefinition(tagTitle, opts) { + this.title = tagTitle; + + this.isIsa = false; // the name of this tag is used to define the doclet's isa property + this.canProvideName = false; // this tag can be used to name the doclet + this.isDocspace = false; // The name of this tag becomes the docspace for the doclet name, like event: + this.canHaveType = false; // this tag can have a {type} + this.canHavePname = false; // this tag can have a parameter-type name + this.canHavePdesc = false; + this.keepsWhitespace = false; + + for (var p in opts) { + if (typeof opts[p] !== 'undefined') { + this[p] = opts[p]; + } + } + + exports.TagDictionary['@'+tagTitle] = this; + } + // event handlers? + TagDefinition.prototype.onDoclet = function(tag, doclet) { + if (this.isIsa) { + if (doclet.isa) { + throw 'Overwriting isa: "'+doclet.isa+'" with "'+this.title+'"'; + } + doclet.isa = this.title; + } + + if (this.canProvideName) { + if (doclet.isa) { + throw 'Overwriting isa: "'+doclet.isa+'" with "'+this.title+'"'; + } + doclet.isa = this.title; + } + } + + new TagDefinition('namespace', { + isIsa: true, + canProvideName: true + }); + + new TagDefinition('constructor', { + isIsa: true, + canProvideName: true + }); + + new TagDefinition('file', { + isIsa: true, + canProvideName: true, + isDocspace: true + }); + + new TagDefinition('event', { + isIsa: true, + canProvideName: true, + isDocspace: true + }); + + new TagDefinition('example', { + keepsWhitespace: true + }); + + new TagDefinition('param', { + canHaveType: true, + canHavePname: true, + canHavePdesc: true + }); + + new TagDefinition('returns', { + canHaveType: true, + canHavePdesc: true + }); + +})(); \ No newline at end of file diff --git a/modules/jsdoc/type.js b/modules/jsdoc/type.js index 38db8ea1..5e2e5c7b 100644 --- a/modules/jsdoc/type.js +++ b/modules/jsdoc/type.js @@ -11,37 +11,37 @@ (function() { /** - @param {string} tagText + @param {string} tagValue @returns {Array.} */ - exports.parse = function(tagText) { - if (typeof tagText !== 'string') { tagText = ''; } + exports.parse = function(tagValue) { + if (typeof tagValue !== 'string') { tagValue = ''; } var type = '', text = '', count = 0; // type expressions start with '{' - if (tagText[0] === '{') { + if (tagValue[0] === '{') { count++; // find matching closer '}' - for (var i = 1, leni = tagText.length; i < leni; i++) { - if (tagText[i] === '{') { count++; } - else if (tagText[i] === '}') { count--; } + for (var i = 1, leni = tagValue.length; i < leni; i++) { + if (tagValue[i] === '{') { count++; } + else if (tagValue[i] === '}') { count--; } if (count === 0) { - type = trim(tagText.slice(1, i)); - text = trim(tagText.slice(i+1)); + type = trim(tagValue.slice(1, i)); + text = trim(tagValue.slice(i+1)); break; } } } - if (type === '') { text = tagText; } + if (type === '') { text = tagValue; } [type, optional] = parseOptional(type); [type, nullable] = parseNullable(type); - + type = parseTypes(type); // make it into an array return [type, text, optional, nullable]; @@ -87,7 +87,7 @@ return types; } - + /** @private */ function trim(text) { return text.replace(/^\s+|\s+$/g, ''); diff --git a/test/runall.js b/test/runall.js index a87b4a36..ffe33169 100644 --- a/test/runall.js +++ b/test/runall.js @@ -10,6 +10,10 @@ load(BASEDIR + '/test/tests/11_tag_namespace.js'); load(BASEDIR + '/test/tests/12_tag_property.js'); load(BASEDIR + '/test/tests/13_tag_method.js'); load(BASEDIR + '/test/tests/14_tag_member.js'); +load(BASEDIR + '/test/tests/15_tag_type.js'); +load(BASEDIR + '/test/tests/16_tag_return.js'); + +load(BASEDIR + '/test/tests/20_tag_file.js'); // see http://visionmedia.github.com/jspec/ JSpec.run({ diff --git a/test/samples/tag_constructor.js b/test/samples/tag_constructor.js new file mode 100644 index 00000000..c84ee6de --- /dev/null +++ b/test/samples/tag_constructor.js @@ -0,0 +1,18 @@ + + /** + @name Foo + @constructor + */ + + /** + @constructor Bar + */ + + +/** @constructor */ +function Pez() { +} + +/** @constructor */ +Qux = function() { +} \ No newline at end of file diff --git a/test/samples/tag_file_1.js b/test/samples/tag_file_1.js new file mode 100644 index 00000000..fedd9d8b --- /dev/null +++ b/test/samples/tag_file_1.js @@ -0,0 +1,21 @@ +/** + * @fileoverview This file is to be used for testing the JSDoc parser + * It is not intended to be an example of good JavaScript OO-programming, + * nor is it intended to fulfill any specific purpose apart from + * demonstrating the functionality of the + * {@link http://sourceforge.net/projects/jsdoc JSDoc} parser + * + * @author Michael Mathews + * @version 0.1 + */ + +function Shape(){ + + this.getClassName = function(){ + return "Shape"; + } + + function addReference(){ + // Do nothing... + } +} \ No newline at end of file diff --git a/test/tests/05_jsdoc_doclet.js b/test/tests/05_jsdoc_doclet.js index 86a6b400..c83ea43e 100644 --- a/test/tests/05_jsdoc_doclet.js +++ b/test/tests/05_jsdoc_doclet.js @@ -16,7 +16,7 @@ expect(doclet.constructor.name).to(eql, 'Doclet'); }); - it('should have a `tagText` method', function() { + it('should have a `tagValue` method', function() { expect(doclet).to(respond_to, 'toObject'); }); @@ -35,20 +35,20 @@ }); }); - describe('The returned value of jsdoc.Doclet#tagText', function() { + describe('The returned value of jsdoc.Doclet#tagValue', function() { it('should be a string', function() { - var returnedValue = doclet.tagText('name'); + var returnedValue = doclet.tagValue('name'); expect(returnedValue).to(be_a, String); }); it('should be the text of the tag that matches the given tag name', function() { - var returnedValue = doclet.tagText('name'); + var returnedValue = doclet.tagValue('name'); expect(returnedValue).to(eql, 'Foo'); }); - it('should be the text of the last tag that matches the given tag name if there are more than 1', function() { - var returnedValue = doclet.tagText('param'); - expect(returnedValue).to(eql, 'b'); + it('should be the text of the first tag that matches the given tag name if there are more than 1', function() { + var returnedValue = doclet.tagValue('param'); + expect(returnedValue).to(eql, 'a'); }); }); diff --git a/test/tests/06_jsdoc_tag.js b/test/tests/06_jsdoc_tag.js index 4a0072df..420db7c6 100644 --- a/test/tests/06_jsdoc_tag.js +++ b/test/tests/06_jsdoc_tag.js @@ -19,8 +19,8 @@ expect(jsdoc.tag).to(be_an, Object); }); - it('should have a `fromTagText` method', function() { - expect(jsdoc.tag).to(respond_to, 'fromTagText'); + it('should have a `fromText` method', function() { + expect(jsdoc.tag).to(respond_to, 'fromText'); }); it('should have a `parse` method', function() { @@ -48,8 +48,7 @@ it('should have a `text` property which is an string', function() { var tag = tags[0]; - expect(tag).to(have_property, 'text'); - expect(tag.text).to(be_an, String); + expect(tag).to(have_property, 'value'); }); it('should have a `type` property which is an array', function() { @@ -73,10 +72,10 @@ }); }); - describe('The tag#text property', function() { + describe('The tag#value property', function() { it('should be set to the text after the @name', function() { var tag = tags[0]; - expect(tag.text).to(eql, 'Hello world'); + expect(tag.value).to(eql, 'Hello world'); }); }); diff --git a/test/tests/10_tag_constructor.js b/test/tests/10_tag_constructor.js index 07a48aff..2294b735 100644 --- a/test/tests/10_tag_constructor.js +++ b/test/tests/10_tag_constructor.js @@ -10,51 +10,51 @@ tag: require('jsdoc/tag'), parser: require('jsdoc/parser') }; - jsdoc.parser.parseFiles(BASEDIR + 'test/tests/10_tag_constructor.js'); + jsdoc.parser.parseFiles(BASEDIR + 'test/samples/tag_constructor.js'); doclets = jsdoc.parser.result; }); - describe('A doclet from a constructor tag with a name tag and no code', function() { - it('should have an `isa` property set to "constructor"', function() { - var doclet = doclets[0].toObject(); - expect(doclet).to(have_property, 'isa'); - expect(doclet.isa).to(eql, 'constructor'); - }); - - it('should have a `name` property set to the given name"', function() { - var doclet = doclets[0].toObject(); - expect(doclet).to(have_property, 'name'); - expect(doclet.name).to(eql, 'Foo'); - }); - }); - - describe('A doclet from a named constructor tag and no code', function() { - it('should have an `isa` property set to "constructor"', function() { - var doclet = doclets[1].toObject(); - expect(doclet).to(have_property, 'isa'); - expect(doclet.isa).to(eql, 'constructor'); - }); - - it('should have a `name` property set to the given name"', function() { - var doclet = doclets[1].toObject(); - expect(doclet).to(have_property, 'name'); - expect(doclet.name).to(eql, 'Bar'); - }); - }); - - describe('A doclet from a constructor tag and named code', function() { - it('should have an `isa` property set to "constructor"', function() { - var doclet = doclets[2].toObject(); - expect(doclet).to(have_property, 'isa'); - expect(doclet.isa).to(eql, 'constructor'); - }); - - it('should have a `name` property set to the given name"', function() { - var doclet = doclets[2].toObject(); - expect(doclet).to(have_property, 'name'); - expect(doclet.name).to(eql, 'Pez'); - }); - }); + describe('A doclet from a constructor tag with a name tag and no code', function() { + it('should have an `isa` property set to "constructor"', function() { + var doclet = doclets[0].toObject(); + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'constructor'); + }); + + it('should have a `name` property set to the given name"', function() { + var doclet = doclets[0].toObject(); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'Foo'); + }); + }); + + describe('A doclet from a named constructor tag and no code', function() { + it('should have an `isa` property set to "constructor"', function() { + var doclet = doclets[1].toObject(); + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'constructor'); + }); + + it('should have a `name` property set to the given name"', function() { + var doclet = doclets[1].toObject(); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'Bar'); + }); + }); + + describe('A doclet from a constructor tag and named code', function() { + it('should have an `isa` property set to "constructor"', function() { + var doclet = doclets[2].toObject(); + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'constructor'); + }); + + it('should have a `name` property set to the given name"', function() { + var doclet = doclets[2].toObject(); + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(eql, 'Pez'); + }); + }); describe('A doclet from a constructor tag and named anonymous function', function() { it('should have an `isa` property set to "constructor"', function() { @@ -71,28 +71,4 @@ }); }); -})(); - -(function testarea() { - - /** - @name Foo - @constructor - */ - - /** - @constructor Bar - */ - - /** - @constructor - */ - function Pez() { - } - - /** - @constructor - */ - Qux = function() { - } })(); \ No newline at end of file diff --git a/test/tests/14_tag_member.js b/test/tests/14_tag_member.js index 3cfd9afb..257fad7d 100644 --- a/test/tests/14_tag_member.js +++ b/test/tests/14_tag_member.js @@ -11,47 +11,60 @@ parser: require('jsdoc/parser') }; jsdoc.parser.parseFiles(BASEDIR + 'test/tests/14_tag_member.js'); - doclets = jsdoc.parser.result; + doclets = jsdoc.parser.result.map(function($){ return $.toObject(); }); + }); describe('A doclet with a method tag and a memberof tag', function() { it('should have an `isa` property set to "method"', function() { - var doclet = doclets[2].toObject(); + var doclet = doclets[2]; expect(doclet).to(have_property, 'isa'); expect(doclet.isa).to(eql, 'method'); }); it('should have a `name` property set to the given name"', function() { - var doclet = doclets[2].toObject(); + var doclet = doclets[2]; expect(doclet).to(have_property, 'name'); expect(doclet.name).to(eql, 'fah'); }); it('should have a `memberof` property set to the given member name', function() { - var doclet = doclets[2].toObject(); + var doclet = doclets[2]; expect(doclet).to(have_property, 'memberof'); - expect(doclet.memberof).to(eql, 'foo'); + expect(doclet.memberof).to(eql, 'foo#'); + }); + + it('should have a `path` property set to the parent+member names', function() { + var doclet = doclets[2]; + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'foo#fah'); }); }); describe('A doclet with a property tag and a member tag', function() { it('should have an `isa` property set to "property"', function() { - var doclet = doclets[3].toObject(); + var doclet = doclets[3]; expect(doclet).to(have_property, 'isa'); expect(doclet.isa).to(eql, 'property'); }); it('should have a `name` property set to the given name"', function() { - var doclet = doclets[3].toObject(); + var doclet = doclets[3]; expect(doclet).to(have_property, 'name'); expect(doclet.name).to(eql, 'bah'); }); it('should have a `memberof` property set to the given member name', function() { - var doclet = doclets[3].toObject(); + var doclet = doclets[3]; expect(doclet).to(have_property, 'memberof'); expect(doclet.memberof).to(eql, 'bar'); }); + + it('should have a `path` property set to the parent+member names', function() { + var doclet = doclets[3]; + expect(doclet).to(have_property, 'path'); + expect(doclet.path).to(eql, 'bar.bah'); + }); }); }); @@ -65,7 +78,7 @@ /** @method fah - @memberof foo + @memberof foo# */ /** diff --git a/test/tests/15_tag_type.js b/test/tests/15_tag_type.js new file mode 100644 index 00000000..3c143ad6 --- /dev/null +++ b/test/tests/15_tag_type.js @@ -0,0 +1,67 @@ +(function() { + var jsdoc, + doclets; + + JSpec.describe('@type', function() { + + before(function() { + // docsets can only be created by parsers + jsdoc = { + tag: require('jsdoc/tag'), + parser: require('jsdoc/parser') + }; + jsdoc.parser.parseFiles(BASEDIR + 'test/tests/15_tag_type.js'); + + doclets = jsdoc.parser.result.map(function($){ return $.toObject(); }); + }); + + describe('A doclet with a type tag whose value is a simple string like "number"', function() { + it('should have an `type` property set to string "number"', function() { + var doclet = doclets[2]; + expect(doclet).to(have_property, 'type'); + expect(doclet.type).to(eql, 'number'); + }); + }); + + describe('A doclet with a type tag whose value is a series of piped strings like "number | Array."', function() { + it('should have an `type` property set to [number, Array.]', function() { + var doclet = doclets[3]; + expect(doclet).to(have_property, 'type'); + + expect(doclet.type).to(eql, ['number', 'Array.']); + }); + }); + + describe('A doclet with a type tag whose value contains braces like "{number|function(string:a, string:b){}:number}"', function() { + it('should have an `type` property set to [number, Array.]', function() { + var doclet = doclets[4]; + expect(doclet).to(have_property, 'type'); + + expect(doclet.type).to(eql, ['number', 'function(string:a, string:b){}:number']); + }); + }); + }); +})(); + +(function testarea() { + + /** @namespace foo */ + + /** @constructor bar */ + + /** + @property foo#fah + @type number + */ + + /** + @property foo#fahfah + @type number | Array. + */ + + /** + @property bar.bah + @type {number|function(string:a, string:b){}:number} + */ + +})(); \ No newline at end of file diff --git a/test/tests/16_tag_return.js b/test/tests/16_tag_return.js new file mode 100644 index 00000000..7303c327 --- /dev/null +++ b/test/tests/16_tag_return.js @@ -0,0 +1,69 @@ +(function() { + var jsdoc, + doclets; + + JSpec.describe('@return', function() { + + before(function() { + // docsets can only be created by parsers + jsdoc = { + tag: require('jsdoc/tag'), + parser: require('jsdoc/parser') + }; + jsdoc.parser.parseFiles(BASEDIR + 'test/tests/16_tag_return.js'); + + doclets = jsdoc.parser.result.map(function($){ return $.toObject(); }); + }); + + describe('A doclet with a returns tag whose value has a type and desc', function() { + it('should have an `returns` property', function() { + var doclet = doclets[0]; + expect(doclet).to(have_property, 'returns'); + }); + }); + + describe('The returns value of that doclet', function() { + it('should have an `type` property set to the given type', function() { + var returns = doclets[0].returns; + expect(returns).to(have_property, 'type'); + expect(returns.type).to(eql, ['number']); + }); + + it('should have an `desc` property set to the given desc', function() { + var returns = doclets[0].returns; + expect(returns).to(have_property, 'desc'); + expect(returns.desc).to(eql, 'The size of the foo.'); + }); + }); + + describe('A doclet with a (synonym) return tag whose value has a desc', function() { + it('should have an `returns` property', function() { + var doclet = doclets[1]; + expect(doclet).to(have_property, 'returns'); + }); + }); + + describe('The returns value of that doclet', function() { + it('should have an `desc` property set to the given desc', function() { + var returns = doclets[1].returns; + expect(returns).to(have_property, 'desc'); + expect(returns.desc).to(eql, 'So a horse walks into a....'); + }); + }); + + }); +})(); + +(function testarea() { + + /** + @function foo + @returns {number} The size of the foo. + */ + + /** + @function bar + @return So a horse walks into a.... + */ + +})(); \ No newline at end of file diff --git a/test/tests/20_tag_file.js b/test/tests/20_tag_file.js new file mode 100644 index 00000000..ea318ca9 --- /dev/null +++ b/test/tests/20_tag_file.js @@ -0,0 +1,32 @@ +(function() { + var jsdoc, + doclets; + + JSpec.describe('@file', 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/tag_file_1.js'); + + doclets = jsdoc.parser.result.map(function($){ return $.toObject(); }); + }); + + describe('A doclet with a fileoverview tag and no name tag', function() { + it('should have an `isa` property set to "file"', function() { + var doclet = doclets[0]; + expect(doclet).to(have_property, 'isa'); + expect(doclet.isa).to(eql, 'file'); + }); + + it('should have an `name` property set to a string equal to the files name', function() { + var doclet = doclets[0]; + expect(doclet).to(have_property, 'name'); + expect(doclet.name).to(match, /test\/samples\/tag_file_1\.js$/); + }); + }); + }); +})();