2 * @fileoverview Main Doctrine object
3 * @author Yusuke Suzuki <utatane.tea@gmail.com>
4 * @author Dan Tao <daniel.tao@gmail.com>
5 * @author Andrew Eisenberg <andrew@eisenberg.as>
17 esutils = require('esutils');
18 typed = require('./typed');
19 utility = require('./utility');
21 function sliceSource(source, index, last) {
22 return source.slice(index, last);
25 hasOwnProperty = (function () {
26 var func = Object.prototype.hasOwnProperty;
27 return function hasOwnProperty(obj, name) {
28 return func.call(obj, name);
31 function shallowCopy(obj) {
34 if (obj.hasOwnProperty(key)) {
41 function isASCIIAlphanumeric(ch) {
42 return (ch >= 0x61 /* 'a' */ && ch <= 0x7A /* 'z' */) ||
43 (ch >= 0x41 /* 'A' */ && ch <= 0x5A /* 'Z' */) ||
44 (ch >= 0x30 /* '0' */ && ch <= 0x39 /* '9' */);
47 function isParamTitle(title) {
48 return title === 'param' || title === 'argument' || title === 'arg';
51 function isReturnTitle(title) {
52 return title === 'return' || title === 'returns';
55 function isProperty(title) {
56 return title === 'property' || title === 'prop';
59 function isNameParameterRequired(title) {
60 return isParamTitle(title) || isProperty(title) ||
61 title === 'alias' || title === 'this' || title === 'mixes' || title === 'requires';
64 function isAllowedName(title) {
65 return isNameParameterRequired(title) || title === 'const' || title === 'constant';
68 function isAllowedNested(title) {
69 return isProperty(title) || isParamTitle(title);
72 function isAllowedOptional(title) {
73 return isProperty(title) || isParamTitle(title);
76 function isTypeParameterRequired(title) {
77 return isParamTitle(title) || isReturnTitle(title) ||
78 title === 'define' || title === 'enum' ||
79 title === 'implements' || title === 'this' ||
80 title === 'type' || title === 'typedef' || isProperty(title);
83 // Consider deprecation instead using 'isTypeParameterRequired' and 'Rules' declaration to pick when a type is optional/required
84 // This would require changes to 'parseType'
85 function isAllowedType(title) {
86 return isTypeParameterRequired(title) || title === 'throws' || title === 'const' || title === 'constant' ||
87 title === 'namespace' || title === 'member' || title === 'var' || title === 'module' ||
88 title === 'constructor' || title === 'class' || title === 'extends' || title === 'augments' ||
89 title === 'public' || title === 'private' || title === 'protected';
92 // A regex character class that contains all whitespace except linebreak characters (\r, \n, \u2028, \u2029)
93 var WHITESPACE = '[ \\f\\t\\v\\u00a0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000\\ufeff]';
95 var STAR_MATCHER = '(' + WHITESPACE + '*(?:\\*' + WHITESPACE + '?)?)(.+|[\r\n\u2028\u2029])';
97 function unwrapComment(doc) {
98 // JSDoc comment is following form
105 replace(/^\/\*\*?/, '').
107 replace(/\*\/$/, '').
108 // remove ' * ' at the beginning of a line
109 replace(new RegExp(STAR_MATCHER, 'g'), '$2').
110 // remove trailing whitespace
115 * Converts an index in an "unwrapped" JSDoc comment to the corresponding index in the original "wrapped" version
116 * @param {string} originalSource The original wrapped comment
117 * @param {number} unwrappedIndex The index of a character in the unwrapped string
118 * @returns {number} The index of the corresponding character in the original wrapped string
120 function convertUnwrappedCommentIndex(originalSource, unwrappedIndex) {
121 var replacedSource = originalSource.replace(/^\/\*\*?/, '');
122 var numSkippedChars = 0;
123 var matcher = new RegExp(STAR_MATCHER, 'g');
126 while ((match = matcher.exec(replacedSource))) {
127 numSkippedChars += match[1].length;
129 if (match.index + match[0].length > unwrappedIndex + numSkippedChars) {
130 return unwrappedIndex + numSkippedChars + originalSource.length - replacedSource.length;
134 return originalSource.replace(/\*\/$/, '').replace(/\s*$/, '').length;
139 (function (exports) {
151 var ch = source.charCodeAt(index);
153 if (esutils.code.isLineTerminator(ch) && !(ch === 0x0D /* '\r' */ && source.charCodeAt(index) === 0x0A /* '\n' */)) {
156 return String.fromCharCode(ch);
159 function scanTitle() {
164 while (index < length && isASCIIAlphanumeric(source.charCodeAt(index))) {
171 function seekContent() {
172 var ch, waiting, last = index;
175 while (last < length) {
176 ch = source.charCodeAt(last);
177 if (esutils.code.isLineTerminator(ch) && !(ch === 0x0D /* '\r' */ && source.charCodeAt(last + 1) === 0x0A /* '\n' */)) {
179 } else if (waiting) {
180 if (ch === 0x40 /* '@' */) {
183 if (!esutils.code.isWhiteSpace(ch)) {
192 // type expression may have nest brace, such as,
193 // { { ok: string } }
195 // therefore, scanning type expression with balancing braces.
196 function parseType(title, last, addRange) {
197 var ch, brace, type, startIndex, direct = false;
201 while (index < last) {
202 ch = source.charCodeAt(index);
203 if (esutils.code.isWhiteSpace(ch)) {
205 } else if (ch === 0x7B /* '{' */) {
209 // this is direct pattern
220 // type expression { is found
223 while (index < last) {
224 ch = source.charCodeAt(index);
225 if (esutils.code.isLineTerminator(ch)) {
228 if (ch === 0x7D /* '}' */) {
234 } else if (ch === 0x7B /* '{' */) {
245 // braces is not balanced
246 return utility.throwError('Braces are not balanced');
249 if (isAllowedOptional(title)) {
250 return typed.parseParamType(type, {startIndex: convertIndex(startIndex), range: addRange});
253 return typed.parseType(type, {startIndex: convertIndex(startIndex), range: addRange});
256 function scanIdentifier(last) {
258 if (!esutils.code.isIdentifierStartES5(source.charCodeAt(index)) && !source[index].match(/[0-9]/)) {
261 identifier = advance();
262 while (index < last && esutils.code.isIdentifierPartES5(source.charCodeAt(index))) {
263 identifier += advance();
268 function skipWhiteSpace(last) {
269 while (index < last && (esutils.code.isWhiteSpace(source.charCodeAt(index)) || esutils.code.isLineTerminator(source.charCodeAt(index)))) {
274 function parseName(last, allowBrackets, allowNestedParams) {
280 skipWhiteSpace(last);
286 if (source.charCodeAt(index) === 0x5B /* '[' */) {
295 name += scanIdentifier(last);
297 if (allowNestedParams) {
298 if (source.charCodeAt(index) === 0x3A /* ':' */ && (
300 name === 'external' ||
303 name += scanIdentifier(last);
306 if(source.charCodeAt(index) === 0x5B /* '[' */ && source.charCodeAt(index + 1) === 0x5D /* ']' */){
310 while (source.charCodeAt(index) === 0x2E /* '.' */ ||
311 source.charCodeAt(index) === 0x2F /* '/' */ ||
312 source.charCodeAt(index) === 0x23 /* '#' */ ||
313 source.charCodeAt(index) === 0x2D /* '-' */ ||
314 source.charCodeAt(index) === 0x7E /* '~' */) {
316 name += scanIdentifier(last);
321 skipWhiteSpace(last);
322 // do we have a default value for this?
323 if (source.charCodeAt(index) === 0x3D /* '=' */) {
324 // consume the '='' symbol
326 skipWhiteSpace(last);
329 var bracketDepth = 1;
331 // scan in the default value
332 while (index < last) {
333 ch = source.charCodeAt(index);
335 if (esutils.code.isWhiteSpace(ch)) {
337 skipWhiteSpace(last);
338 ch = source.charCodeAt(index);
342 if (ch === 0x27 /* ''' */) {
346 if (insideString === '\'') {
352 if (ch === 0x22 /* '"' */) {
356 if (insideString === '"') {
362 if (ch === 0x5B /* '[' */) {
364 } else if (ch === 0x5D /* ']' */ &&
365 --bracketDepth === 0) {
373 skipWhiteSpace(last);
375 if (index >= last || source.charCodeAt(index) !== 0x5D /* ']' */) {
376 // we never found a closing ']'
380 // collect the last ']'
387 function skipToTag() {
388 while (index < length && source.charCodeAt(index) !== 0x40 /* '@' */) {
391 if (index >= length) {
394 utility.assert(source.charCodeAt(index) === 0x40 /* '@' */);
398 function convertIndex(rangeIndex) {
399 if (source === originalSource) {
402 return convertUnwrappedCommentIndex(originalSource, rangeIndex);
405 function TagParser(options, title) {
406 this._options = options;
407 this._title = title.toLowerCase();
412 if (this._options.lineNumbers) {
413 this._tag.lineNumber = lineNumber;
415 this._first = index - title.length - 1;
417 // space to save special information for title parsers.
421 // addError(err, ...)
422 TagParser.prototype.addError = function addError(errorText) {
423 var args = Array.prototype.slice.call(arguments, 1),
424 msg = errorText.replace(
426 function (whole, index) {
427 utility.assert(index < args.length, 'Message reference must be in range');
432 if (!this._tag.errors) {
433 this._tag.errors = [];
436 utility.throwError(msg);
438 this._tag.errors.push(msg);
442 TagParser.prototype.parseType = function () {
443 // type required titles
444 if (isTypeParameterRequired(this._title)) {
446 this._tag.type = parseType(this._title, this._last, this._options.range);
447 if (!this._tag.type) {
448 if (!isParamTitle(this._title) && !isReturnTitle(this._title)) {
449 if (!this.addError('Missing or invalid tag type')) {
455 this._tag.type = null;
456 if (!this.addError(error.message)) {
460 } else if (isAllowedType(this._title)) {
463 this._tag.type = parseType(this._title, this._last, this._options.range);
465 //For optional types, lets drop the thrown error when we hit the end of the file
471 TagParser.prototype._parseNamePath = function (optional) {
473 name = parseName(this._last, sloppy && isAllowedOptional(this._title), true);
476 if (!this.addError('Missing or invalid tag name')) {
481 this._tag.name = name;
485 TagParser.prototype.parseNamePath = function () {
486 return this._parseNamePath(false);
489 TagParser.prototype.parseNamePathOptional = function () {
490 return this._parseNamePath(true);
494 TagParser.prototype.parseName = function () {
497 // param, property requires name
498 if (isAllowedName(this._title)) {
499 this._tag.name = parseName(this._last, sloppy && isAllowedOptional(this._title), isAllowedNested(this._title));
500 if (!this._tag.name) {
501 if (!isNameParameterRequired(this._title)) {
505 // it's possible the name has already been parsed but interpreted as a type
506 // it's also possible this is a sloppy declaration, in which case it will be
508 if (isParamTitle(this._title) && this._tag.type && this._tag.type.name) {
509 this._extra.name = this._tag.type;
510 this._tag.name = this._tag.type.name;
511 this._tag.type = null;
513 if (!this.addError('Missing or invalid tag name')) {
518 name = this._tag.name;
519 if (name.charAt(0) === '[' && name.charAt(name.length - 1) === ']') {
520 // extract the default value if there is one
521 // example: @param {string} [somebody=John Doe] description
522 assign = name.substring(1, name.length - 1).split('=');
523 if (assign.length > 1) {
524 this._tag['default'] = assign.slice(1).join('=');
526 this._tag.name = assign[0];
528 // convert to an optional type
529 if (this._tag.type && this._tag.type.type !== 'OptionalType') {
531 type: 'OptionalType',
532 expression: this._tag.type
543 TagParser.prototype.parseDescription = function parseDescription() {
544 var description = sliceSource(source, index, this._last).trim();
546 if ((/^-\s+/).test(description)) {
547 description = description.substring(2);
549 this._tag.description = description;
554 TagParser.prototype.parseCaption = function parseDescription() {
555 var description = sliceSource(source, index, this._last).trim();
556 var captionStartTag = '<caption>';
557 var captionEndTag = '</caption>';
558 var captionStart = description.indexOf(captionStartTag);
559 var captionEnd = description.indexOf(captionEndTag);
560 if (captionStart >= 0 && captionEnd >= 0) {
561 this._tag.caption = description.substring(
562 captionStart + captionStartTag.length, captionEnd).trim();
563 this._tag.description = description.substring(captionEnd + captionEndTag.length).trim();
565 this._tag.description = description;
570 TagParser.prototype.parseKind = function parseKind() {
585 kind = sliceSource(source, index, this._last).trim();
586 this._tag.kind = kind;
587 if (!hasOwnProperty(kinds, kind)) {
588 if (!this.addError('Invalid kind name \'%0\'', kind)) {
595 TagParser.prototype.parseAccess = function parseAccess() {
597 access = sliceSource(source, index, this._last).trim();
598 this._tag.access = access;
599 if (access !== 'private' && access !== 'protected' && access !== 'public') {
600 if (!this.addError('Invalid access name \'%0\'', access)) {
607 TagParser.prototype.parseThis = function parseThis() {
608 // this name may be a name expression (e.g. {foo.bar}),
609 // an union (e.g. {foo.bar|foo.baz}) or a name path (e.g. foo.bar)
610 var value = sliceSource(source, index, this._last).trim();
611 if (value && value.charAt(0) === '{') {
612 var gotType = this.parseType();
613 if (gotType && this._tag.type.type === 'NameExpression' || this._tag.type.type === 'UnionType') {
614 this._tag.name = this._tag.type.name;
617 return this.addError('Invalid name for this');
620 return this.parseNamePath();
624 TagParser.prototype.parseVariation = function parseVariation() {
626 text = sliceSource(source, index, this._last).trim();
627 variation = parseFloat(text, 10);
628 this._tag.variation = variation;
629 if (isNaN(variation)) {
630 if (!this.addError('Invalid variation \'%0\'', text)) {
637 TagParser.prototype.ensureEnd = function () {
638 var shouldBeEmpty = sliceSource(source, index, this._last).trim();
640 if (!this.addError('Unknown content \'%0\'', shouldBeEmpty)) {
647 TagParser.prototype.epilogue = function epilogue() {
650 description = this._tag.description;
651 // un-fix potentially sloppy declaration
652 if (isAllowedOptional(this._title) && !this._tag.type && description && description.charAt(0) === '[') {
653 this._tag.type = this._extra.name;
654 if (!this._tag.name) {
655 this._tag.name = undefined;
659 if (!this.addError('Missing or invalid tag name')) {
669 // http://usejsdoc.org/tags-access.html
670 'access': ['parseAccess'],
671 // http://usejsdoc.org/tags-alias.html
672 'alias': ['parseNamePath', 'ensureEnd'],
673 // http://usejsdoc.org/tags-augments.html
674 'augments': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
675 // http://usejsdoc.org/tags-constructor.html
676 'constructor': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
677 // Synonym: http://usejsdoc.org/tags-constructor.html
678 'class': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
679 // Synonym: http://usejsdoc.org/tags-extends.html
680 'extends': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
681 // http://usejsdoc.org/tags-example.html
682 'example': ['parseCaption'],
683 // http://usejsdoc.org/tags-deprecated.html
684 'deprecated': ['parseDescription'],
685 // http://usejsdoc.org/tags-global.html
686 'global': ['ensureEnd'],
687 // http://usejsdoc.org/tags-inner.html
688 'inner': ['ensureEnd'],
689 // http://usejsdoc.org/tags-instance.html
690 'instance': ['ensureEnd'],
691 // http://usejsdoc.org/tags-kind.html
692 'kind': ['parseKind'],
693 // http://usejsdoc.org/tags-mixes.html
694 'mixes': ['parseNamePath', 'ensureEnd'],
695 // http://usejsdoc.org/tags-mixin.html
696 'mixin': ['parseNamePathOptional', 'ensureEnd'],
697 // http://usejsdoc.org/tags-member.html
698 'member': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
699 // http://usejsdoc.org/tags-method.html
700 'method': ['parseNamePathOptional', 'ensureEnd'],
701 // http://usejsdoc.org/tags-module.html
702 'module': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
703 // Synonym: http://usejsdoc.org/tags-method.html
704 'func': ['parseNamePathOptional', 'ensureEnd'],
705 // Synonym: http://usejsdoc.org/tags-method.html
706 'function': ['parseNamePathOptional', 'ensureEnd'],
707 // Synonym: http://usejsdoc.org/tags-member.html
708 'var': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
709 // http://usejsdoc.org/tags-name.html
710 'name': ['parseNamePath', 'ensureEnd'],
711 // http://usejsdoc.org/tags-namespace.html
712 'namespace': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
713 // http://usejsdoc.org/tags-private.html
714 'private': ['parseType', 'parseDescription'],
715 // http://usejsdoc.org/tags-protected.html
716 'protected': ['parseType', 'parseDescription'],
717 // http://usejsdoc.org/tags-public.html
718 'public': ['parseType', 'parseDescription'],
719 // http://usejsdoc.org/tags-readonly.html
720 'readonly': ['ensureEnd'],
721 // http://usejsdoc.org/tags-requires.html
722 'requires': ['parseNamePath', 'ensureEnd'],
723 // http://usejsdoc.org/tags-since.html
724 'since': ['parseDescription'],
725 // http://usejsdoc.org/tags-static.html
726 'static': ['ensureEnd'],
727 // http://usejsdoc.org/tags-summary.html
728 'summary': ['parseDescription'],
729 // http://usejsdoc.org/tags-this.html
730 'this': ['parseThis', 'ensureEnd'],
731 // http://usejsdoc.org/tags-todo.html
732 'todo': ['parseDescription'],
733 // http://usejsdoc.org/tags-typedef.html
734 'typedef': ['parseType', 'parseNamePathOptional'],
735 // http://usejsdoc.org/tags-variation.html
736 'variation': ['parseVariation'],
737 // http://usejsdoc.org/tags-version.html
738 'version': ['parseDescription']
741 TagParser.prototype.parse = function parse() {
742 var i, iz, sequences, method;
747 if (!this.addError('Missing or invalid title')) {
752 // Seek to content last index.
753 this._last = seekContent(this._title);
755 if (this._options.range) {
756 this._tag.range = [this._first, source.slice(0, this._last).replace(/\s*$/, '').length].map(convertIndex);
759 if (hasOwnProperty(Rules, this._title)) {
760 sequences = Rules[this._title];
763 sequences = ['parseType', 'parseName', 'parseDescription', 'epilogue'];
766 for (i = 0, iz = sequences.length; i < iz; ++i) {
767 method = sequences[i];
768 if (!this[method]()) {
776 function parseTag(options) {
777 var title, parser, tag;
787 // construct tag parser
788 parser = new TagParser(options, title);
789 tag = parser.parse();
791 // Seek global index to end of this tag.
792 while (index < parser._last) {
803 function scanJSDocDescription(preserveWhitespace) {
804 var description = '', ch, atAllowed;
807 while (index < length) {
808 ch = source.charCodeAt(index);
810 if (atAllowed && ch === 0x40 /* '@' */) {
814 if (esutils.code.isLineTerminator(ch)) {
816 } else if (atAllowed && !esutils.code.isWhiteSpace(ch)) {
820 description += advance();
823 return preserveWhitespace ? description : description.trim();
826 function parse(comment, options) {
827 var tags = [], tag, description, interestingTags, i, iz;
829 if (options === undefined) {
833 if (typeof options.unwrap === 'boolean' && options.unwrap) {
834 source = unwrapComment(comment);
839 originalSource = comment;
841 // array of relevant tags
843 if (Array.isArray(options.tags)) {
844 interestingTags = { };
845 for (i = 0, iz = options.tags.length; i < iz; i++) {
846 if (typeof options.tags[i] === 'string') {
847 interestingTags[options.tags[i]] = true;
849 utility.throwError('Invalid "tags" parameter: ' + options.tags);
853 utility.throwError('Invalid "tags" parameter: ' + options.tags);
857 length = source.length;
860 recoverable = options.recoverable;
861 sloppy = options.sloppy;
862 strict = options.strict;
864 description = scanJSDocDescription(options.preserveWhitespace);
867 tag = parseTag(options);
871 if (!interestingTags || interestingTags.hasOwnProperty(tag.title)) {
877 description: description,
881 exports.parse = parse;
884 exports.version = utility.VERSION;
885 exports.parse = jsdoc.parse;
886 exports.parseType = typed.parseType;
887 exports.parseParamType = typed.parseParamType;
888 exports.unwrapComment = unwrapComment;
889 exports.Syntax = shallowCopy(typed.Syntax);
890 exports.Error = utility.DoctrineError;
892 Syntax: exports.Syntax,
893 parseType: typed.parseType,
894 parseParamType: typed.parseParamType,
895 stringify: typed.stringify
898 /* vim: set sw=4 ts=4 et tw=80 : */