diff --git a/lib/jsdoc/tag.js b/lib/jsdoc/tag.js index 349b15c5..ac261585 100644 --- a/lib/jsdoc/tag.js +++ b/lib/jsdoc/tag.js @@ -41,13 +41,14 @@ function trim(text, newlines) { @param {object=} meta */ exports.Tag = function(tagTitle, tagBody, meta) { - var tagDef = jsdoc.tag.dictionary.lookUp(tagTitle); meta = meta || {}; this.originalTitle = trim(tagTitle); /** The title part of the tag: @title text */ this.title = jsdoc.tag.dictionary.normalise( this.originalTitle ); + + var tagDef = jsdoc.tag.dictionary.lookUp(this.title); /** The text part of the tag: @title text */ this.text = trim(tagBody, tagDef.keepsWhitespace); @@ -65,10 +66,15 @@ exports.Tag = function(tagTitle, tagBody, meta) { var tagType = jsdoc.tag.type.parse(this.text, tagDef.canHaveName, tagDef.canHaveType); - if (tagType.type && tagType.type.length) { - this.value.type = { - names: tagType.type - }; + // It is possible for a tag to *not* have a type but still have + // optional or defaultvalue, e.g. '@param [foo]'. + // Although tagType.type.length == 0 we should still copy the other properties. + if (tagType.type) { + if (tagType.type.length) { + this.value.type = { + names: tagType.type + }; + } this.value.optional = tagType.optional; this.value.nullable = tagType.nullable; this.value.variable = tagType.variable; diff --git a/lib/jsdoc/tag/dictionary.js b/lib/jsdoc/tag/dictionary.js index 033e6134..18190502 100644 --- a/lib/jsdoc/tag/dictionary.js +++ b/lib/jsdoc/tag/dictionary.js @@ -33,13 +33,15 @@ TagDefinition.prototype.synonym = function(synonymName) { dictionary = { /** @function */ defineTag: function(title, opts) { - _definitions[title] = new TagDefinition(title, opts); + var def = new TagDefinition(title, opts); + // all the other dictionary functions use normalised names; we should too. + _definitions[def.title] = def; if (opts.isNamespace) { - _namespaces.push(title); + _namespaces.push(def.title); } - return _definitions[title]; + return _definitions[def.title]; }, /** @function */ @@ -55,8 +57,11 @@ dictionary = { /** @function */ isNamespace: function(kind) { - if ( _namespaces.indexOf(kind) !== -1) { - return true; + if (kind) { + kind = dictionary.normalise(kind); + if ( _namespaces.indexOf(kind) !== -1) { + return true; + } } return false; diff --git a/test/fixtures/paramtag.js b/test/fixtures/paramtag.js index db431bfc..c6fd831c 100644 --- a/test/fixtures/paramtag.js +++ b/test/fixtures/paramtag.js @@ -39,3 +39,9 @@ function split(delimiter) { */ function commit(atomic) { } + +/** + * @param [async=true] - whether to be asynchronous + */ +function request(async) { +} diff --git a/test/specs/jsdoc/tag.js b/test/specs/jsdoc/tag.js index af71b0a0..a324a9c3 100644 --- a/test/specs/jsdoc/tag.js +++ b/test/specs/jsdoc/tag.js @@ -1,33 +1,162 @@ /*global describe: true, env: true, it: true */ describe("jsdoc/tag", function() { - /*jshint evil: true */ - - // TODO: more tests + var jsdoc = { + tag: require('jsdoc/tag'), + dictionary: require('jsdoc/tag/dictionary'), + type: require('jsdoc/tag/type') + }; - var lenient = !!env.opts.lenient, - log = eval(console.log); - - function badTag() { - var Tag = require("jsdoc/tag").Tag; - var tag = new Tag("name"); - return tag; - } - - afterEach(function() { - env.opts.lenient = lenient; - console.log = log; + it('should exist', function() { + expect(jsdoc.tag).toBeDefined(); + expect(typeof jsdoc.tag).toBe('object'); }); - it("throws an exception for bad tags if the lenient option is not enabled", function() { - env.opts.lenient = false; - - expect(badTag).toThrow(); + it('should export a Tag function', function() { + expect(jsdoc.tag.Tag).toBeDefined(); + expect(typeof jsdoc.tag.Tag).toBe('function'); }); - - it("doesn't throw an exception for bad tags if the lenient option is enabled", function() { - console.log = function() {}; - env.opts.lenient = true; - expect(badTag).not.toThrow(); + describe('Tag', function() { + var meta = {lineno: 1, filename: 'asdf.js'}, + desc = 'lalblakd lkjasdlib\n lija', + text = '{!number} [foo=1] - ' + desc, + textEg = 'Asdf\n' + + ' * myFunction(1, 2); // returns 3\n' + + ' * myFunction(3, 4); // returns 7\n'; + var tagArg = new jsdoc.tag.Tag('arg ', text, meta), // <-- a symonym of param, space in the title. + tagParam = new jsdoc.tag.Tag('param', '[foo=1]', meta), // no type, but has optional and defaultvalue. + tagEg = new jsdoc.tag.Tag('example', textEg, meta), // <-- for keepsWhitespace + tagType = new jsdoc.tag.Tag('type', 'MyType ', meta); // <-- for onTagText + + it("should have a 'originalTitle' property, a string", function() { + expect(tagArg.originalTitle).toBeDefined(); + expect(typeof tagArg.originalTitle).toBe('string'); + }); + + it("'originalTitle' property should be the initial tag title, trimmed of whitespace", function() { + expect(tagArg.originalTitle).toBe('arg'); + expect(tagEg.originalTitle).toBe('example'); + }); + + it("should have a 'title' property, a string", function() { + expect(tagArg.title).toBeDefined(); + expect(typeof tagArg.title).toBe('string'); + }); + + it("'title' property should be the normalised tag title", function() { + expect(tagArg.title).toBe(jsdoc.dictionary.normalise(tagArg.originalTitle)); + expect(tagEg.title).toBe(jsdoc.dictionary.normalise(tagEg.originalTitle)); + }); + + it("should have a 'text' property. a string", function () { + expect(tagArg.text).toBeDefined(); + expect(typeof tagArg.text).toBe('string'); + }); + + it("should have a 'value' property", function () { + expect(tagArg.value).toBeDefined(); + expect(tagEg.value).toBeDefined(); + expect(tagType.value).toBeDefined(); + }); + + describe("'text' property", function() { + it("'text' property should be the trimmed tag text, with all leading and trailing space removed unless tagDef.keepsWhitespace", function() { + // @example has keepsWhitespace, @param doesn't. + // should realy use module:jsdoc/tag~trim here but it's private. + expect(tagArg.text).toBe(text.replace(/^\s+|\s+$/g, '')); + expect(tagEg.text).toBe(textEg.replace(/^[\n\r\f]+|[\n\r\f]+$/g, '')); + }); + + it("'text' property should have onTagText run on it if it has it.", function() { + var def = jsdoc.dictionary.lookUp('type'); + expect(def.onTagText).toBeDefined(); + expect(typeof def.onTagText).toBe('function'); + + // @type adds {} around the type if necessary. + expect(tagType.text).toBeDefined(); + expect(tagType.text).toBe(def.onTagText('MyType')); + }); + }); + + describe("'value' property", function() { + it("'value' property should equal tag text if tagDef.canHaveType and canHaveName are both false", function() { + // @example can't have type or name + expect(typeof tagEg.value).toBe('string'); + expect(tagEg.value).toBe(tagEg.text); + }); + + it("'value' property should be an object if tagDef can have type or name", function () { + expect(typeof tagType.value).toBe('object'); + expect(typeof tagArg.value).toBe('object'); + }); + + function verifyTagType(tag) { + var def = jsdoc.dictionary.lookUp(tag.title); + expect(def).not.toBe(false); + var info = jsdoc.type.parse(tag.text, def.canHaveName, def.canHaveType); + + var props_that_should_be_copied = ['optional', 'nullable', 'variable', 'defaultvalue']; + for (var i = 0; i < props_that_should_be_copied.length; ++i) { + var prop = props_that_should_be_copied[i]; + if (info.hasOwnProperty(prop)) { + expect(tag.value[prop]).toBe(info[prop]); + } + } + if (info.type && info.type.length) { + expect(tag.value.type).toBeDefined(); + expect(typeof tag.value.type).toBe('object'); + expect(tag.value.type.names).toBeDefined(); + expect(tag.value.type.names).toEqual(info.type); + } + } + it("if the tag has a type, tag.value should contain the type information", function() { + // we assume jsdoc/tag/type.parse works (it has its own tests to verify this); + verifyTagType(tagType); + verifyTagType(tagArg); + verifyTagType(tagParam); + }); + + it("if the tag has a description beyond the name/type, this should be in tag.value.description", function() { + expect(tagType.value.description).not.toBeDefined(); + + expect(tagArg.value.description).toBeDefined(); + expect(tagArg.value.description).toBe(desc); + }); + + it("if the tag can have a name, it should be stored in tag.value.name", function() { + expect(tagArg.value.name).toBeDefined(); + expect(tagArg.value.name).toBe('foo'); + + expect(tagType.value.name).not.toBeDefined(); + }); + }); + + // further tests for this sort of thing are in jsdoc/tag/validator.js tests. + describe("tag validating", function() { + /*jshint evil: true */ + var lenient = !!env.opts.lenient; + + function badTag() { + var tag = new jsdoc.tag.Tag("name"); + return tag; + } + + afterEach(function() { + env.opts.lenient = lenient; + }); + + it("throws an exception for bad tags if the lenient option is not enabled", function() { + env.opts.lenient = false; + + expect(badTag).toThrow(); + }); + + it("doesn't throw an exception for bad tags if the lenient option is enabled", function() { + spyOn(console, 'log'); + env.opts.lenient = true; + + expect(badTag).not.toThrow(); + }); + }); }); -}); \ No newline at end of file +}); diff --git a/test/specs/jsdoc/tag/dictionary.js b/test/specs/jsdoc/tag/dictionary.js index af8f72d0..b6e0544c 100644 --- a/test/specs/jsdoc/tag/dictionary.js +++ b/test/specs/jsdoc/tag/dictionary.js @@ -1,3 +1,98 @@ -describe("jsdoc/tag/dictionary", function() { - //TODO -}); \ No newline at end of file +describe('jsdoc/tag/dictionary', function() { + var dictionary = require('jsdoc/tag/dictionary'); + + it('should exist', function() { + expect(dictionary).toBeDefined(); + expect(typeof dictionary).toBe('object'); + }); + + it('should export a defineTag function', function() { + expect(dictionary.defineTag).toBeDefined(); + expect(typeof dictionary.defineTag).toBe('function'); + }); + + it('should export a lookUp function', function() { + expect(dictionary.lookUp).toBeDefined(); + expect(typeof dictionary.lookUp).toBe('function'); + }); + + it('should export a isNamespace function', function() { + expect(dictionary.isNamespace).toBeDefined(); + expect(typeof dictionary.isNamespace).toBe('function'); + }); + + it('should export a normalise function', function() { + expect(dictionary.normalise).toBeDefined(); + expect(typeof dictionary.normalise).toBe('function'); + }); + + // TODO: should really remove this tag from the dictionary after, but how? + var tagOptions = { + canHaveValue: true, + isNamespace: true + }, + tagTitle = 'testTag', + tagSynonym = 'testTag2'; + var def = dictionary.defineTag(tagTitle, tagOptions).synonym(tagSynonym); + // Should really test TagDefinition but they are private. + // Instead, we'll just tests all the properties we expect of it. + describe("defineTag", function() { + + // Since TagDefinition is private, I'll just test for its properties here. + it("returns an object with 'title' property", function() { + expect(typeof def).toBe('object'); + // how to test? + expect(def.title).toBeDefined(); + expect(typeof def.title).toBe('string'); + expect(def.title).toBe(dictionary.normalise(tagTitle)); + }); + + it("returned object has all the tag properties copied over", function() { + for (var prop in tagOptions) { + if (tagOptions.hasOwnProperty(prop)) { + expect(def[prop]).toBe(tagOptions[prop]); + } + } + }); + }); + + describe("lookUp", function() { + it("retrieves definition when using the tag's canonical name", function() { + expect(dictionary.lookUp(tagTitle)).toBe(def); + }); + + it("retrieves definition when using a synonym", function() { + expect(dictionary.lookUp(tagSynonym)).toBe(def); + }); + + it("returns FALSE when a tag is not found", function() { + expect(dictionary.lookUp('lkjas1l24jk')).toBe(false); + }); + }); + + describe("isNamespace", function() { + it("returns whether a tag is a namespace when using its canonical name", function() { + expect(dictionary.isNamespace(tagTitle)).toBe(true); + }); + + it("returns whether a tag is a namespace when using its synonym", function() { + expect(dictionary.isNamespace(tagSynonym)).toBe(true); + }); + + it("non-existent tags or non-namespace tags should return false", function() { + expect(dictionary.isNamespace('see')).toBe(false); + expect(dictionary.isNamespace('lkjasd90034')).toBe(false); + }); + }); + + describe("normalise", function() { + it("should return the tag's title if it is not a synonym", function() { + expect(dictionary.normalise('FooBar')).toBe('foobar'); + expect(dictionary.normalise(tagTitle)).toBe(def.title); + }); + + it("should return the canonical name of a tag if the synonym is normalised", function() { + expect(dictionary.normalise(tagSynonym)).toBe(def.title); + }); + }); +}); diff --git a/test/specs/jsdoc/tag/dictionary/definitions.js b/test/specs/jsdoc/tag/dictionary/definitions.js new file mode 100644 index 00000000..4abae895 --- /dev/null +++ b/test/specs/jsdoc/tag/dictionary/definitions.js @@ -0,0 +1,15 @@ +describe('jsdoc/tag/dictionary/definitions', function() { + var type = require('jsdoc/tag/dictionary/definitions'); + + it('should exist', function() { + expect(type).toBeDefined(); + expect(typeof type).toBe('object'); + }); + + it('should export a defineTags function', function() { + expect(type.defineTags).toBeDefined(); + expect(typeof type.defineTags).toBe('function'); + }); + // whole bit of dictionary.defineTags...but it just calls dictionary.defineTag + // and if I validate that then the rest is automatically validated? +}); diff --git a/test/specs/jsdoc/tag/type.js b/test/specs/jsdoc/tag/type.js index a5ed0830..bec572f8 100644 --- a/test/specs/jsdoc/tag/type.js +++ b/test/specs/jsdoc/tag/type.js @@ -92,6 +92,10 @@ describe('jsdoc/tag/type', function() { desc = '{string=} [foo]'; info = jsdoc.tag.type.parse(desc, true, true); expect(info.optional).toBe(true); + + desc = '[foo]'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.optional).toBe(true); }); it('should return the types as an array', function() { diff --git a/test/specs/jsdoc/tag/validator.js b/test/specs/jsdoc/tag/validator.js index 4dd4be63..56070edb 100644 --- a/test/specs/jsdoc/tag/validator.js +++ b/test/specs/jsdoc/tag/validator.js @@ -1,3 +1,94 @@ -describe("jsdoc/tag/validator", function() { - //TODO -}); \ No newline at end of file +describe('jsdoc/tag/validator', function() { + var validator = require('jsdoc/tag/validator'), + tag = require('jsdoc/tag'); + + it('should exist', function() { + expect(validator).toBeDefined(); + expect(typeof validator).toBe('object'); + }); + + it('should export a validate function', function() { + expect(validator.validate).toBeDefined(); + expect(typeof validator.validate).toBe('function'); + }); + + // Note: various Error classes are private so we just test whether *any* + // error was thrown, not against particular types (e.g. UnknownTagError). + describe('validate', function() { + var lenient = !!env.opts.lenient, + allowUnknown = !!env.conf.tags.allowUnknownTags, + badTag = {title: 'lkjasdlkjfb'}, + meta = {filename: 'asdf.js', lineno: 1}, + goodTag = new tag.Tag('name', 'MyDocletName', meta), // mustHaveValue + goodTag2 = new tag.Tag('ignore', '', meta); // mustNotHaveValue + + function validateTag(tag) { + return function() { validator.validate(tag, meta); }; + } + + beforeEach(function() { + spyOn(console, 'log'); + }); + + afterEach(function() { + env.opts.lenient = lenient; + env.conf.tags.allowUnknownTags = allowUnknown; + }); + + it("throws an error if the tag is not in the dictionary, conf.tags.allowUnknownTags is false and lenient is false", function() { + env.opts.lenient = false; + env.conf.tags.allowUnknownTags = false; + expect(validateTag(badTag)).toThrow(); + }); + + it("throws NO error if the tag is not in the dictionary, conf.tags.allowUnknownTags is false and lenient is true", function() { + env.opts.lenient = true; + env.conf.tags.allowUnknownTags = false; + expect(validateTag(badTag)).not.toThrow(); + }); + + it("doesn't throw an error if the tag is not in the dictionary and conf.tags.allowUnknownTags is true, regardless of lenience", function() { + // if it doesn't throw an error with lenient false, then it won't throw it with lenient true (we have + // tested lenient already in util/error.js) + env.opts.lenient = false; + env.conf.tags.allowUnknownTags = true; + expect(validateTag(badTag)).not.toThrow(); + }); + + it("throws no error for valid tags", function() { + env.opts.lenient = false; + expect(validateTag(goodTag)).not.toThrow(); + expect(validateTag(goodTag2)).not.toThrow(); + }); + + it("throws an error if the tag has no text but .mustHaveValue is true and lenient is false, or none if it's true", function() { + // the name tag has .mustHaveValue. + var oldText = goodTag.text; + delete goodTag.text; + + env.opts.lenient = false; + expect(validateTag(goodTag)).toThrow(); + + env.opts.lenient = true; + expect(validateTag(goodTag)).not.toThrow(); + + goodTag.text = oldText; + }); + + it("throws an error if the tag has text but .mustNotHaveValue is true and lenient is false, or none if it's true", function() { + var oldVal = goodTag2.mustNotHaveValue, + text = goodTag2.text; + goodTag2.mustNotHaveValue = true; + goodTag2.text = goodTag2.text || 'asdf'; + + env.opts.lenient = false; + expect(validateTag(goodTag2)).toThrow(); + + env.opts.lenient = true; + expect(validateTag(goodTag2)).not.toThrow(); + + goodTag2.mustNotHaveValue = oldVal; + goodTag2.text = oldVal; + }); + }); +}); diff --git a/test/specs/tags/paramtag.js b/test/specs/tags/paramtag.js index 10e847fa..7145c83f 100644 --- a/test/specs/tags/paramtag.js +++ b/test/specs/tags/paramtag.js @@ -6,7 +6,8 @@ describe("@param tag", function() { getElement = docSet.getByLongname('getElement')[0], combine = docSet.getByLongname('combine')[0], split = docSet.getByLongname('split')[0], - commit = docSet.getByLongname('commit')[0]; + commit = docSet.getByLongname('commit')[0], + request = docSet.getByLongname('request')[0]; it('When a symbol has an @param tag with a type before the name, the doclet has a params property that includes that param.', function() { expect(typeof find.params).toBe('object'); @@ -62,7 +63,18 @@ describe("@param tag", function() { expect(commit.params[0].description).toBe('If true make the commit atomic.'); }); + it('When a symbol has a @param tag with no type but a name that indicates a default value or optional type, this is copied over to the params property.', function() { + expect(typeof request.params).toBe('object'); + expect(request.params.length).toBe(1); + expect(request.params[0].type).toBeUndefined(); + expect(request.params[0].name).toBe('async'); + expect(request.params[0].defaultvalue).toBe('true'); + expect(request.params[0].optional).toBe(true); + expect(request.params[0].description).toBe('whether to be asynchronous'); + }); + it('When a symbol has an @param tag with no name and a name is given in the code, the doclet has a params property that includes that param with the name from the code.', function() { expect(commit.params[0].name).toBe('atomic'); }); -}); \ No newline at end of file + +});