2 Copyright (C) 2012-2014 Yusuke Suzuki <utatane.tea@gmail.com>
3 Copyright (C) 2014 Dan Tao <daniel.tao@gmail.com>
4 Copyright (C) 2013 Andrew Eisenberg <andrew@eisenberg.as>
6 Redistribution and use in source and binary forms, with or without
7 modification, are permitted provided that the following conditions are met:
9 * Redistributions of source code must retain the above copyright
10 notice, this list of conditions and the following disclaimer.
11 * Redistributions in binary form must reproduce the above copyright
12 notice, this list of conditions and the following disclaimer in the
13 documentation and/or other materials provided with the distribution.
15 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
19 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 esutils = require('esutils');
38 isArray = require('isarray');
39 typed = require('./typed');
40 utility = require('./utility');
42 function sliceSource(source, index, last) {
43 return source.slice(index, last);
46 hasOwnProperty = (function () {
47 var func = Object.prototype.hasOwnProperty;
48 return function hasOwnProperty(obj, name) {
49 return func.call(obj, name);
53 function shallowCopy(obj) {
56 if (obj.hasOwnProperty(key)) {
63 function isASCIIAlphanumeric(ch) {
64 return (ch >= 0x61 /* 'a' */ && ch <= 0x7A /* 'z' */) ||
65 (ch >= 0x41 /* 'A' */ && ch <= 0x5A /* 'Z' */) ||
66 (ch >= 0x30 /* '0' */ && ch <= 0x39 /* '9' */);
69 function isParamTitle(title) {
70 return title === 'param' || title === 'argument' || title === 'arg';
73 function isProperty(title) {
74 return title === 'property' || title === 'prop';
77 function isNameParameterRequired(title) {
78 return isParamTitle(title) || isProperty(title) ||
79 title === 'alias' || title === 'this' || title === 'mixes' || title === 'requires';
82 function isAllowedName(title) {
83 return isNameParameterRequired(title) || title === 'const' || title === 'constant';
86 function isAllowedNested(title) {
87 return isProperty(title) || isParamTitle(title);
90 function isTypeParameterRequired(title) {
91 return isParamTitle(title) || title === 'define' || title === 'enum' ||
92 title === 'implements' || title === 'return' ||
93 title === 'this' || title === 'type' || title === 'typedef' ||
94 title === 'returns' || isProperty(title);
97 // Consider deprecation instead using 'isTypeParameterRequired' and 'Rules' declaration to pick when a type is optional/required
98 // This would require changes to 'parseType'
99 function isAllowedType(title) {
100 return isTypeParameterRequired(title) || title === 'throws' || title === 'const' || title === 'constant' ||
101 title === 'namespace' || title === 'member' || title === 'var' || title === 'module' ||
102 title === 'constructor' || title === 'class' || title === 'extends' || title === 'augments' ||
103 title === 'public' || title === 'private' || title === 'protected';
107 return str.replace(/^\s+/, '').replace(/\s+$/, '');
110 function unwrapComment(doc) {
111 // JSDoc comment is following form
115 // remove /**, */ and *
125 doc = doc.replace(/^\/\*\*?/, '').replace(/\*\/$/, '');
131 while (index < len) {
132 ch = doc.charCodeAt(index);
135 if (esutils.code.isLineTerminator(ch)) {
136 result += String.fromCharCode(ch);
137 } else if (ch === 0x2A /* '*' */) {
139 } else if (!esutils.code.isWhiteSpace(ch)) {
140 result += String.fromCharCode(ch);
146 if (!esutils.code.isWhiteSpace(ch)) {
147 result += String.fromCharCode(ch);
149 mode = esutils.code.isLineTerminator(ch) ? BEFORE_STAR : AFTER_STAR;
153 result += String.fromCharCode(ch);
154 if (esutils.code.isLineTerminator(ch)) {
167 (function (exports) {
178 var ch = source.charCodeAt(index);
180 if (esutils.code.isLineTerminator(ch) && !(ch === 0x0D /* '\r' */ && source.charCodeAt(index) === 0x0A /* '\n' */)) {
183 return String.fromCharCode(ch);
186 function scanTitle() {
191 while (index < length && isASCIIAlphanumeric(source.charCodeAt(index))) {
198 function seekContent() {
199 var ch, waiting, last = index;
202 while (last < length) {
203 ch = source.charCodeAt(last);
204 if (esutils.code.isLineTerminator(ch) && !(ch === 0x0D /* '\r' */ && source.charCodeAt(last + 1) === 0x0A /* '\n' */)) {
207 } else if (waiting) {
208 if (ch === 0x40 /* '@' */) {
211 if (!esutils.code.isWhiteSpace(ch)) {
220 // type expression may have nest brace, such as,
221 // { { ok: string } }
223 // therefore, scanning type expression with balancing braces.
224 function parseType(title, last) {
225 var ch, brace, type, direct = false;
229 while (index < last) {
230 ch = source.charCodeAt(index);
231 if (esutils.code.isWhiteSpace(ch)) {
233 } else if (ch === 0x7B /* '{' */) {
237 // this is direct pattern
248 // type expression { is found
251 while (index < last) {
252 ch = source.charCodeAt(index);
253 if (esutils.code.isLineTerminator(ch)) {
256 if (ch === 0x7D /* '}' */) {
262 } else if (ch === 0x7B /* '{' */) {
270 // braces is not balanced
271 return utility.throwError('Braces are not balanced');
274 if (isParamTitle(title)) {
275 return typed.parseParamType(type);
277 return typed.parseType(type);
280 function scanIdentifier(last) {
282 if (!esutils.code.isIdentifierStart(source.charCodeAt(index))) {
285 identifier = advance();
286 while (index < last && esutils.code.isIdentifierPart(source.charCodeAt(index))) {
287 identifier += advance();
292 function skipWhiteSpace(last) {
293 while (index < last && (esutils.code.isWhiteSpace(source.charCodeAt(index)) || esutils.code.isLineTerminator(source.charCodeAt(index)))) {
298 function parseName(last, allowBrackets, allowNestedParams) {
299 var name = '', useBrackets;
301 skipWhiteSpace(last);
307 if (allowBrackets && source.charCodeAt(index) === 0x5B /* '[' */) {
312 if (!esutils.code.isIdentifierStart(source.charCodeAt(index))) {
316 name += scanIdentifier(last);
318 if (allowNestedParams) {
319 if (source.charCodeAt(index) === 0x3A /* ':' */ && (
321 name === 'external' ||
324 name += scanIdentifier(last);
327 while (source.charCodeAt(index) === 0x2E /* '.' */ ||
328 source.charCodeAt(index) === 0x23 /* '#' */ ||
329 source.charCodeAt(index) === 0x7E /* '~' */) {
331 name += scanIdentifier(last);
336 // do we have a default value for this?
337 if (source.charCodeAt(index) === 0x3D /* '=' */) {
339 // consume the '='' symbol
341 // scan in the default value
342 while (index < last && source.charCodeAt(index) !== 0x5D /* ']' */) {
347 if (index >= last || source.charCodeAt(index) !== 0x5D /* ']' */) {
348 // we never found a closing ']'
352 // collect the last ']'
359 function skipToTag() {
360 while (index < length && source.charCodeAt(index) !== 0x40 /* '@' */) {
363 if (index >= length) {
366 utility.assert(source.charCodeAt(index) === 0x40 /* '@' */);
370 function TagParser(options, title) {
371 this._options = options;
377 if (this._options.lineNumbers) {
378 this._tag.lineNumber = lineNumber;
381 // space to save special information for title parsers.
385 // addError(err, ...)
386 TagParser.prototype.addError = function addError(errorText) {
387 var args = Array.prototype.slice.call(arguments, 1),
388 msg = errorText.replace(
390 function (whole, index) {
391 utility.assert(index < args.length, 'Message reference must be in range');
396 if (!this._tag.errors) {
397 this._tag.errors = [];
400 utility.throwError(msg);
402 this._tag.errors.push(msg);
406 TagParser.prototype.parseType = function () {
407 // type required titles
408 if (isTypeParameterRequired(this._title)) {
410 this._tag.type = parseType(this._title, this._last);
411 if (!this._tag.type) {
412 if (!isParamTitle(this._title)) {
413 if (!this.addError('Missing or invalid tag type')) {
419 this._tag.type = null;
420 if (!this.addError(error.message)) {
424 } else if (isAllowedType(this._title)) {
427 this._tag.type = parseType(this._title, this._last);
429 //For optional types, lets drop the thrown error when we hit the end of the file
435 TagParser.prototype._parseNamePath = function (optional) {
437 name = parseName(this._last, sloppy && isParamTitle(this._title), true);
440 if (!this.addError('Missing or invalid tag name')) {
445 this._tag.name = name;
449 TagParser.prototype.parseNamePath = function () {
450 return this._parseNamePath(false);
453 TagParser.prototype.parseNamePathOptional = function () {
454 return this._parseNamePath(true);
458 TagParser.prototype.parseName = function () {
461 // param, property requires name
462 if (isAllowedName(this._title)) {
463 this._tag.name = parseName(this._last, sloppy && isParamTitle(this._title), isAllowedNested(this._title));
464 if (!this._tag.name) {
465 if (!isNameParameterRequired(this._title)) {
469 // it's possible the name has already been parsed but interpreted as a type
470 // it's also possible this is a sloppy declaration, in which case it will be
472 if (isParamTitle(this._title) && this._tag.type && this._tag.type.name) {
473 this._extra.name = this._tag.type;
474 this._tag.name = this._tag.type.name;
475 this._tag.type = null;
477 if (!this.addError('Missing or invalid tag name')) {
482 name = this._tag.name;
483 if (name.charAt(0) === '[' && name.charAt(name.length - 1) === ']') {
484 // extract the default value if there is one
485 // example: @param {string} [somebody=John Doe] description
486 assign = name.substring(1, name.length - 1).split('=');
488 this._tag['default'] = assign[1];
490 this._tag.name = assign[0];
492 // convert to an optional type
493 if (this._tag.type && this._tag.type.type !== 'OptionalType') {
495 type: 'OptionalType',
496 expression: this._tag.type
506 TagParser.prototype.parseDescription = function parseDescription() {
507 var description = trim(sliceSource(source, index, this._last));
509 if ((/^-\s+/).test(description)) {
510 description = description.substring(2);
512 this._tag.description = description;
517 TagParser.prototype.parseKind = function parseKind() {
532 kind = trim(sliceSource(source, index, this._last));
533 this._tag.kind = kind;
534 if (!hasOwnProperty(kinds, kind)) {
535 if (!this.addError('Invalid kind name \'%0\'', kind)) {
542 TagParser.prototype.parseAccess = function parseAccess() {
544 access = trim(sliceSource(source, index, this._last));
545 this._tag.access = access;
546 if (access !== 'private' && access !== 'protected' && access !== 'public') {
547 if (!this.addError('Invalid access name \'%0\'', access)) {
554 TagParser.prototype.parseVariation = function parseVariation() {
556 text = trim(sliceSource(source, index, this._last));
557 variation = parseFloat(text, 10);
558 this._tag.variation = variation;
559 if (isNaN(variation)) {
560 if (!this.addError('Invalid variation \'%0\'', text)) {
567 TagParser.prototype.ensureEnd = function () {
568 var shouldBeEmpty = trim(sliceSource(source, index, this._last));
570 if (!this.addError('Unknown content \'%0\'', shouldBeEmpty)) {
577 TagParser.prototype.epilogue = function epilogue() {
580 description = this._tag.description;
581 // un-fix potentially sloppy declaration
582 if (isParamTitle(this._title) && !this._tag.type && description && description.charAt(0) === '[') {
583 this._tag.type = this._extra.name;
584 this._tag.name = undefined;
587 if (!this.addError('Missing or invalid tag name')) {
597 // http://usejsdoc.org/tags-access.html
598 'access': ['parseAccess'],
599 // http://usejsdoc.org/tags-alias.html
600 'alias': ['parseNamePath', 'ensureEnd'],
601 // http://usejsdoc.org/tags-augments.html
602 'augments': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
603 // http://usejsdoc.org/tags-constructor.html
604 'constructor': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
605 // Synonym: http://usejsdoc.org/tags-constructor.html
606 'class': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
607 // Synonym: http://usejsdoc.org/tags-extends.html
608 'extends': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
609 // http://usejsdoc.org/tags-deprecated.html
610 'deprecated': ['parseDescription'],
611 // http://usejsdoc.org/tags-global.html
612 'global': ['ensureEnd'],
613 // http://usejsdoc.org/tags-inner.html
614 'inner': ['ensureEnd'],
615 // http://usejsdoc.org/tags-instance.html
616 'instance': ['ensureEnd'],
617 // http://usejsdoc.org/tags-kind.html
618 'kind': ['parseKind'],
619 // http://usejsdoc.org/tags-mixes.html
620 'mixes': ['parseNamePath', 'ensureEnd'],
621 // http://usejsdoc.org/tags-mixin.html
622 'mixin': ['parseNamePathOptional', 'ensureEnd'],
623 // http://usejsdoc.org/tags-member.html
624 'member': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
625 // http://usejsdoc.org/tags-method.html
626 'method': ['parseNamePathOptional', 'ensureEnd'],
627 // http://usejsdoc.org/tags-module.html
628 'module': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
629 // Synonym: http://usejsdoc.org/tags-method.html
630 'func': ['parseNamePathOptional', 'ensureEnd'],
631 // Synonym: http://usejsdoc.org/tags-method.html
632 'function': ['parseNamePathOptional', 'ensureEnd'],
633 // Synonym: http://usejsdoc.org/tags-member.html
634 'var': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
635 // http://usejsdoc.org/tags-name.html
636 'name': ['parseNamePath', 'ensureEnd'],
637 // http://usejsdoc.org/tags-namespace.html
638 'namespace': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
639 // http://usejsdoc.org/tags-private.html
640 'private': ['parseType', 'parseDescription'],
641 // http://usejsdoc.org/tags-protected.html
642 'protected': ['parseType', 'parseDescription'],
643 // http://usejsdoc.org/tags-public.html
644 'public': ['parseType', 'parseDescription'],
645 // http://usejsdoc.org/tags-readonly.html
646 'readonly': ['ensureEnd'],
647 // http://usejsdoc.org/tags-requires.html
648 'requires': ['parseNamePath', 'ensureEnd'],
649 // http://usejsdoc.org/tags-since.html
650 'since': ['parseDescription'],
651 // http://usejsdoc.org/tags-static.html
652 'static': ['ensureEnd'],
653 // http://usejsdoc.org/tags-summary.html
654 'summary': ['parseDescription'],
655 // http://usejsdoc.org/tags-this.html
656 'this': ['parseNamePath', 'ensureEnd'],
657 // http://usejsdoc.org/tags-todo.html
658 'todo': ['parseDescription'],
659 // http://usejsdoc.org/tags-variation.html
660 'variation': ['parseVariation'],
661 // http://usejsdoc.org/tags-version.html
662 'version': ['parseDescription']
665 TagParser.prototype.parse = function parse() {
666 var i, iz, sequences, method;
670 if (!this.addError('Missing or invalid title')) {
675 // Seek to content last index.
676 this._last = seekContent(this._title);
678 if (hasOwnProperty(Rules, this._title)) {
679 sequences = Rules[this._title];
682 sequences = ['parseType', 'parseName', 'parseDescription', 'epilogue'];
685 for (i = 0, iz = sequences.length; i < iz; ++i) {
686 method = sequences[i];
687 if (!this[method]()) {
692 // Seek global index to end of this tag.
697 function parseTag(options) {
708 // construct tag parser
709 parser = new TagParser(options, title);
710 return parser.parse();
717 function scanJSDocDescription() {
718 var description = '', ch, atAllowed;
721 while (index < length) {
722 ch = source.charCodeAt(index);
724 if (atAllowed && ch === 0x40 /* '@' */) {
728 if (esutils.code.isLineTerminator(ch)) {
730 } else if (atAllowed && !esutils.code.isWhiteSpace(ch)) {
734 description += advance();
736 return trim(description);
739 function parse(comment, options) {
740 var tags = [], tag, description, interestingTags, i, iz;
742 if (options === undefined) {
746 if (typeof options.unwrap === 'boolean' && options.unwrap) {
747 source = unwrapComment(comment);
752 // array of relevant tags
754 if (isArray(options.tags)) {
755 interestingTags = { };
756 for (i = 0, iz = options.tags.length; i < iz; i++) {
757 if (typeof options.tags[i] === 'string') {
758 interestingTags[options.tags[i]] = true;
760 utility.throwError('Invalid "tags" parameter: ' + options.tags);
764 utility.throwError('Invalid "tags" parameter: ' + options.tags);
768 length = source.length;
771 recoverable = options.recoverable;
772 sloppy = options.sloppy;
773 strict = options.strict;
775 description = scanJSDocDescription();
778 tag = parseTag(options);
782 if (!interestingTags || interestingTags.hasOwnProperty(tag.title)) {
788 description: description,
792 exports.parse = parse;
795 exports.version = utility.VERSION;
796 exports.parse = jsdoc.parse;
797 exports.parseType = typed.parseType;
798 exports.parseParamType = typed.parseParamType;
799 exports.unwrapComment = unwrapComment;
800 exports.Syntax = shallowCopy(typed.Syntax);
801 exports.Error = utility.DoctrineError;
803 Syntax: exports.Syntax,
804 parseType: typed.parseType,
805 parseParamType: typed.parseParamType,
806 stringify: typed.stringify
809 /* vim: set sw=4 ts=4 et tw=80 : */