'use strict'; /* @flow */ var doctrine = require('doctrine-temporary-fork'); var parseMarkdown = require('./parse_markdown'); /** * Flatteners: these methods simplify the structure of JSDoc comments * into a flat object structure, parsing markdown and extracting * information where appropriate. * @private */ var flatteners = { abstract: flattenBoolean, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ access: function(result, tag) { // doctrine ensures that tag.access is valid result.access = tag.access; }, alias: flattenName, arg: synonym('param'), argument: synonym('param'), /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ augments: function(result, tag) { // Google variation of augments/extends tag: // uses type with brackets instead of name. // https://github.com/google/closure-library/issues/746 if (!tag.name && tag.type && tag.type.name) { tag.name = tag.type.name; } if (!tag.name) { console.error('@extends from complex types is not supported yet'); // eslint-disable-line no-console return; } result.augments.push(tag); }, author: flattenDescription, borrows: todo, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ callback: function(result, tag) { result.kind = 'typedef'; if (tag.description) { result.name = tag.description; } result.type = { type: 'NameExpression', name: 'Function' }; }, class: flattenKindShorthand, classdesc: flattenMarkdownDescription, const: synonym('constant'), constant: flattenKindShorthand, constructor: synonym('class'), constructs: todo, copyright: flattenMarkdownDescription, default: todo, defaultvalue: synonym('default'), deprecated(result, tag) { let description = tag.description || 'This is deprecated.'; result.deprecated = parseMarkdown(description); }, flattenMarkdownDescription, desc: synonym('description'), description: flattenMarkdownDescription, emits: synonym('fires'), enum: todo, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ event: function(result, tag) { result.kind = 'event'; if (tag.description) { result.name = tag.description; } }, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ example: function(result, tag) { if (!tag.description) { result.errors.push({ message: '@example without code', commentLineNumber: tag.lineNumber }); return; } var example /*: CommentExample */ = { description: tag.description }; if (tag.caption) { example.caption = parseMarkdown(tag.caption); } result.examples.push(example); }, exception: synonym('throws'), exports: todo, extends: synonym('augments'), /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ external: function(result, tag) { result.kind = 'external'; if (tag.description) { result.name = tag.description; } }, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ file: function(result, tag) { result.kind = 'file'; if (tag.description) { result.description = parseMarkdown(tag.description); } }, fileoverview: synonym('file'), fires: todo, func: synonym('function'), function: flattenKindShorthand, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ global: function(result) { result.scope = 'global'; }, host: synonym('external'), ignore: flattenBoolean, implements: todo, inheritdoc: todo, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ inner: function(result) { result.scope = 'inner'; }, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ instance: function(result) { result.scope = 'instance'; }, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ interface: function(result, tag) { result.interface = true; if (tag.description) { result.name = tag.description; } }, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ kind: function(result, tag) { // doctrine ensures that tag.kind is valid result.kind = tag.kind; }, lends: flattenDescription, license: flattenDescription, listens: todo, member: flattenKindShorthand, memberof: flattenDescription, method: synonym('function'), mixes: todo, mixin: flattenKindShorthand, module: flattenKindShorthand, name: flattenName, namespace: flattenKindShorthand, override: flattenBoolean, overview: synonym('file'), /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ param: function(result, tag) { var param /*: CommentTag */ = { title: 'param', name: tag.name, lineNumber: tag.lineNumber // TODO: remove }; if (tag.description) { param.description = parseMarkdown(tag.description); } if (tag.type) { param.type = tag.type; } if (tag.default) { param.default = tag.default; } result.params.push(param); }, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ private: function(result) { result.access = 'private'; }, prop: synonym('property'), /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ property: function(result, tag) { var property /*: CommentTag */ = { title: 'property', name: tag.name, lineNumber: tag.lineNumber // TODO: remove }; if (tag.description) { property.description = parseMarkdown(tag.description); } if (tag.type) { property.type = tag.type; } result.properties.push(property); }, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ protected: function(result) { result.access = 'protected'; }, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ public: function(result) { result.access = 'public'; }, readonly: flattenBoolean, requires: todo, return: synonym('returns'), /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ returns: function(result, tag) { var returns /*: CommentTag */ = { description: parseMarkdown(tag.description), title: 'returns' }; if (tag.type) { returns.type = tag.type; } result.returns.push(returns); }, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ see: function(result, tag) { result.sees.push(parseMarkdown(tag.description)); }, since: flattenDescription, /** * Parse tag * @private * @param {Object} result target comment * @returns {undefined} has side-effects */ static: function(result) { result.scope = 'static'; }, summary: flattenMarkdownDescription, this: todo, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ throws: function(result, tag) { var throws = {}; if (tag.description) { throws.description = parseMarkdown(tag.description); } if (tag.type) { throws.type = tag.type; } result.throws.push(throws); }, /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ todo: function(result, tag) { result.todos.push(parseMarkdown(tag.description)); }, tutorial: todo, type: todo, typedef: flattenKindShorthand, var: synonym('member'), /** * Parse tag * @private * @param {Object} result target comment * @param {Object} tag the tag * @returns {undefined} has side-effects */ variation: function(result, tag) { result.variation = tag.variation; }, version: flattenDescription, virtual: synonym('abstract') }; /** * A no-op function for unsupported tags * @returns {undefined} does nothing */ function todo() {} /** * Generate a function that curries a destination key for a flattener * @private * @param {string} key the eventual destination key * @returns {Function} a flattener that remembers that key */ function synonym(key) { return function(result, tag) { return flatteners[key](result, tag, key); }; } /** * Treat the existence of a tag as a sign to mark `key` as true in the result * @private * @param {Object} result the documentation object * @param {Object} tag the tag object, with a name property * @param {string} key destination on the result * @returns {undefined} operates with side-effects */ function flattenBoolean(result, tag, key) { result[key] = true; } /** * Flatten a usable-once name tag into a key * @private * @param {Object} result the documentation object * @param {Object} tag the tag object, with a name property * @param {string} key destination on the result * @returns {undefined} operates with side-effects */ function flattenName(result, tag, key) { result[key] = tag.name; } /** * Flatten a usable-once description tag into a key * @private * @param {Object} result the documentation object * @param {Object} tag the tag object, with a description property * @param {string} key destination on the result * @returns {undefined} operates with side-effects */ function flattenDescription(result, tag, key) { result[key] = tag.description; } /** * Flatten a usable-once description tag into a key and parse it as Markdown * @private * @param {Object} result the documentation object * @param {Object} tag the tag object, with a description property * @param {string} key destination on the result * @returns {undefined} operates with side-effects */ function flattenMarkdownDescription(result, tag, key) { result[key] = parseMarkdown(tag.description); } /** * Parse [kind shorthand](http://usejsdoc.org/tags-kind.html) into * both name and type tags, like `@class [ ]` * * @param {Object} result comment * @param {Object} tag parsed tag * @param {string} key tag * @returns {undefined} operates through side effects * @private */ function flattenKindShorthand(result, tag, key) { result.kind = key; if (tag.name) { result.name = tag.name; } if (tag.type) { result.type = tag.type; } } /** * Parse a comment with doctrine, decorate the result with file position and code * context, handle parsing errors, and fix up various infelicities in the structure * outputted by doctrine. * * The following tags are treated as synonyms for a canonical tag: * * * `@virtual` ⇢ `@abstract` * * `@extends` ⇢ `@augments` * * `@constructor` ⇢ `@class` * * `@const` ⇢ `@constant` * * `@defaultvalue` ⇢ `@default` * * `@desc` ⇢ `@description` * * `@host` ⇢ `@external` * * `@fileoverview`, `@overview` ⇢ `@file` * * `@emits` ⇢ `@fires` * * `@func`, `@method` ⇢ `@function` * * `@var` ⇢ `@member` * * `@arg`, `@argument` ⇢ `@param` * * `@prop` ⇢ `@property` * * `@return` ⇢ `@returns` * * `@exception` ⇢ `@throws` * * `@linkcode`, `@linkplain` ⇢ `@link` * * The following tags are assumed to be singletons, and are flattened * to a top-level property on the result whose value is extracted from * the tag: * * * `@name` * * `@memberof` * * `@classdesc` * * `@kind` * * `@class` * * `@constant` * * `@event` * * `@external` * * `@file` * * `@function` * * `@member` * * `@mixin` * * `@module` * * `@namespace` * * `@typedef` * * `@access` * * `@lends` * * `@description` * * `@summary` * * `@copyright` * * `@deprecated` * * The following tags are flattened to a top-level array-valued property: * * * `@param` (to `params` property) * * `@property` (to `properties` property) * * `@returns` (to `returns` property) * * `@augments` (to `augments` property) * * `@example` (to `examples` property) * * `@throws` (to `throws` property) * * `@see` (to `sees` property) * * `@todo` (to `todos` property) * * The `@global`, `@static`, `@instance`, and `@inner` tags are flattened * to a `scope` property whose value is `"global"`, `"static"`, `"instance"`, * or `"inner"`. * * The `@access`, `@public`, `@protected`, and `@private` tags are flattened * to an `access` property whose value is `"protected"` or `"private"`. * The assumed default value is `"public"`, so `@access public` or `@public` * tags result in no `access` property. * * @param {string} comment input to be parsed * @param {Object} loc location of the input * @param {Object} context code context of the input * @return {Comment} an object conforming to the * [documentation schema](https://github.com/documentationjs/api-json) */ function parseJSDoc( comment /*: string*/, loc /*: ?Object*/, context /*: ?Object*/ ) /*: Comment */ { var result = doctrine.parse(comment, { // have doctrine itself remove the comment asterisks from content unwrap: true, // enable parsing of optional parameters in brackets, JSDoc3 style sloppy: true, // `recoverable: true` is the only way to get error information out recoverable: true, // include line numbers lineNumbers: true }); result.loc = loc; result.context = context; result.augments = []; result.errors = []; result.examples = []; result.params = []; result.properties = []; result.returns = []; result.sees = []; result.throws = []; result.todos = []; if (result.description) { result.description = parseMarkdown(result.description); } result.tags.forEach(function(tag) { if (tag.errors) { for (var j = 0; j < tag.errors.length; j++) { result.errors.push({ message: tag.errors[j] }); } } else if (flatteners[tag.title]) { flatteners[tag.title](result, tag, tag.title); } else { result.errors.push({ message: 'unknown tag @' + tag.title, commentLineNumber: tag.lineNumber }); } }); return result; } module.exports = parseJSDoc;