UNPKG

16.8 kBJavaScriptView Raw
1const doctrine = require('doctrine-temporary-fork');
2const parseMarkdown = require('./parse_markdown');
3
4/**
5 * Flatteners: these methods simplify the structure of JSDoc comments
6 * into a flat object structure, parsing markdown and extracting
7 * information where appropriate.
8 * @private
9 */
10const flatteners = {
11 abstract: flattenBoolean,
12 /**
13 * Parse tag
14 * @private
15 * @param {Object} result target comment
16 * @param {Object} tag the tag
17 * @returns {undefined} has side-effects
18 */
19 access(result, tag) {
20 // doctrine ensures that tag.access is valid
21 result.access = tag.access;
22 },
23 alias: flattenName,
24 arg: synonym('param'),
25 argument: synonym('param'),
26 async: flattenBoolean,
27 /**
28 * Parse tag
29 * @private
30 * @param {Object} result target comment
31 * @param {Object} tag the tag
32 * @returns {undefined} has side-effects
33 */
34 augments(result, tag) {
35 // Google variation of augments/extends tag:
36 // uses type with brackets instead of name.
37 // https://github.com/google/closure-library/issues/746
38 if (!tag.name && tag.type && tag.type.name) {
39 tag.name = tag.type.name;
40 }
41 if (!tag.name) {
42 console.error('@extends from complex types is not supported yet'); // eslint-disable-line no-console
43 return;
44 }
45 result.augments.push(tag);
46 },
47 author: flattenDescription,
48 borrows: todo,
49 /**
50 * Parse tag
51 * @private
52 * @param {Object} result target comment
53 * @param {Object} tag the tag
54 * @returns {undefined} has side-effects
55 */
56 callback(result, tag) {
57 result.kind = 'typedef';
58
59 if (tag.description) {
60 result.name = tag.description;
61 }
62
63 result.type = {
64 type: 'NameExpression',
65 name: 'Function'
66 };
67 },
68 class: flattenKindShorthand,
69 classdesc: flattenMarkdownDescription,
70 const: synonym('constant'),
71 constant: flattenKindShorthand,
72 constructor: synonym('class'),
73 constructs: todo,
74 copyright: flattenMarkdownDescription,
75 default: todo,
76 defaultvalue: synonym('default'),
77 deprecated(result, tag) {
78 const description = tag.description || 'This is deprecated.';
79 result.deprecated = parseMarkdown(description);
80 },
81 flattenMarkdownDescription,
82 desc: synonym('description'),
83 description: flattenMarkdownDescription,
84 emits: synonym('fires'),
85 enum(result, tag) {
86 result.kind = 'enum';
87 result.type = tag.type;
88 },
89 /**
90 * Parse tag
91 * @private
92 * @param {Object} result target comment
93 * @param {Object} tag the tag
94 * @returns {undefined} has side-effects
95 */
96 event(result, tag) {
97 result.kind = 'event';
98
99 if (tag.description) {
100 result.name = tag.description;
101 }
102 },
103 /**
104 * Parse tag
105 * @private
106 * @param {Object} result target comment
107 * @param {Object} tag the tag
108 * @returns {undefined} has side-effects
109 */
110 example(result, tag) {
111 if (!tag.description) {
112 result.errors.push({
113 message: '@example without code',
114 commentLineNumber: tag.lineNumber
115 });
116 return;
117 }
118
119 const example = {
120 description: tag.description
121 };
122
123 if (tag.caption) {
124 example.caption = parseMarkdown(tag.caption);
125 }
126
127 result.examples.push(example);
128 },
129 exception: synonym('throws'),
130 exports: todo,
131 extends: synonym('augments'),
132 /**
133 * Parse tag
134 * @private
135 * @param {Object} result target comment
136 * @param {Object} tag the tag
137 * @returns {undefined} has side-effects
138 */
139 external(result, tag) {
140 result.kind = 'external';
141
142 if (tag.description) {
143 result.name = tag.description;
144 }
145 },
146 /**
147 * Parse tag
148 * @private
149 * @param {Object} result target comment
150 * @param {Object} tag the tag
151 * @returns {undefined} has side-effects
152 */
153 file(result, tag) {
154 result.kind = 'file';
155
156 if (tag.description) {
157 result.description = parseMarkdown(tag.description);
158 }
159 },
160 fileoverview: synonym('file'),
161 fires: todo,
162 func: synonym('function'),
163 function: flattenKindShorthand,
164 generator: flattenBoolean,
165 /**
166 * Parse tag
167 * @private
168 * @param {Object} result target comment
169 * @returns {undefined} has side-effects
170 */
171 global(result) {
172 result.scope = 'global';
173 },
174 hideconstructor: flattenBoolean,
175 host: synonym('external'),
176 ignore: flattenBoolean,
177 implements(result, tag) {
178 // Match @extends/@augments above.
179 if (!tag.name && tag.type && tag.type.name) {
180 tag.name = tag.type.name;
181 }
182
183 result.implements.push(tag);
184 },
185 inheritdoc: todo,
186 /**
187 * Parse tag
188 * @private
189 * @param {Object} result target comment
190 * @returns {undefined} has side-effects
191 */
192 inner(result) {
193 result.scope = 'inner';
194 },
195 /**
196 * Parse tag
197 * @private
198 * @param {Object} result target comment
199 * @returns {undefined} has side-effects
200 */
201 instance(result) {
202 result.scope = 'instance';
203 },
204 /**
205 * Parse tag
206 * @private
207 * @param {Object} result target comment
208 * @param {Object} tag the tag
209 * @returns {undefined} has side-effects
210 */
211 interface(result, tag) {
212 result.kind = 'interface';
213 if (tag.description) {
214 result.name = tag.description;
215 }
216 },
217 /**
218 * Parse tag
219 * @private
220 * @param {Object} result target comment
221 * @param {Object} tag the tag
222 * @returns {undefined} has side-effects
223 */
224 kind(result, tag) {
225 // doctrine ensures that tag.kind is valid
226 result.kind = tag.kind;
227 },
228 lends: flattenDescription,
229 license: flattenDescription,
230 listens: todo,
231 member: flattenKindShorthand,
232 memberof: flattenDescription,
233 method: synonym('function'),
234 mixes: todo,
235 mixin: flattenKindShorthand,
236 module: flattenKindShorthand,
237 name: flattenName,
238 namespace: flattenKindShorthand,
239 override: flattenBoolean,
240 overview: synonym('file'),
241 /**
242 * Parse tag
243 * @private
244 * @param {Object} result target comment
245 * @param {Object} tag the tag
246 * @returns {undefined} has side-effects
247 */
248 param(result, tag) {
249 const param = {
250 title: 'param',
251 name: tag.name,
252 lineNumber: tag.lineNumber // TODO: remove
253 };
254
255 if (tag.description) {
256 param.description = parseMarkdown(tag.description);
257 }
258
259 if (tag.type) {
260 param.type = tag.type;
261 }
262
263 if (tag.default) {
264 param.default = tag.default;
265 if (param.type && param.type.type === 'OptionalType') {
266 param.type = param.type.expression;
267 }
268 }
269
270 result.params.push(param);
271 },
272 /**
273 * Parse tag
274 * @private
275 * @param {Object} result target comment
276 * @returns {undefined} has side-effects
277 */
278 private(result) {
279 result.access = 'private';
280 },
281 prop: synonym('property'),
282 /**
283 * Parse tag
284 * @private
285 * @param {Object} result target comment
286 * @param {Object} tag the tag
287 * @returns {undefined} has side-effects
288 */
289 property(result, tag) {
290 const property = {
291 title: 'property',
292 name: tag.name,
293 lineNumber: tag.lineNumber // TODO: remove
294 };
295
296 if (tag.description) {
297 property.description = parseMarkdown(tag.description);
298 }
299
300 if (tag.type) {
301 property.type = tag.type;
302 }
303
304 result.properties.push(property);
305 },
306 /**
307 * Parse tag
308 * @private
309 * @param {Object} result target comment
310 * @returns {undefined} has side-effects
311 */
312 protected(result) {
313 result.access = 'protected';
314 },
315 /**
316 * Parse tag
317 * @private
318 * @param {Object} result target comment
319 * @returns {undefined} has side-effects
320 */
321 public(result) {
322 result.access = 'public';
323 },
324 readonly: flattenBoolean,
325 requires: todo,
326 return: synonym('returns'),
327 /**
328 * Parse tag
329 * @private
330 * @param {Object} result target comment
331 * @param {Object} tag the tag
332 * @returns {undefined} has side-effects
333 */
334 returns(result, tag) {
335 const returns = {
336 description: parseMarkdown(tag.description),
337 title: 'returns'
338 };
339
340 if (tag.type) {
341 returns.type = tag.type;
342 }
343
344 result.returns.push(returns);
345 },
346 /**
347 * Parse tag
348 * @private
349 * @param {Object} result target comment
350 * @param {Object} tag the tag
351 * @returns {undefined} has side-effects
352 */
353 see(result, tag) {
354 const sees = {
355 description: parseMarkdown(tag.description),
356 title: 'sees'
357 };
358
359 if (tag.type) {
360 sees.type = tag.type;
361 }
362
363 result.sees.push(sees);
364 },
365 since: flattenDescription,
366 /**
367 * Parse tag
368 * @private
369 * @param {Object} result target comment
370 * @returns {undefined} has side-effects
371 */
372 static(result) {
373 result.scope = 'static';
374 },
375 summary: flattenMarkdownDescription,
376 this: todo,
377 /**
378 * Parse tag
379 * @private
380 * @param {Object} result target comment
381 * @param {Object} tag the tag
382 * @returns {undefined} has side-effects
383 */
384 throws(result, tag) {
385 const throws = {};
386
387 if (tag.description) {
388 throws.description = parseMarkdown(tag.description);
389 }
390
391 if (tag.type) {
392 throws.type = tag.type;
393 }
394
395 result.throws.push(throws);
396 },
397 /**
398 * Parse tag
399 * @private
400 * @param {Object} result target comment
401 * @param {Object} tag the tag
402 * @returns {undefined} has side-effects
403 */
404 todo(result, tag) {
405 result.todos.push(parseMarkdown(tag.description));
406 },
407 tutorial: todo,
408 type(result, tag) {
409 result.type = tag.type;
410 },
411 typedef: flattenKindShorthand,
412 var: synonym('member'),
413 /**
414 * Parse tag
415 * @private
416 * @param {Object} result target comment
417 * @param {Object} tag the tag
418 * @returns {undefined} has side-effects
419 */
420 variation(result, tag) {
421 result.variation = tag.variation;
422 },
423 version: flattenDescription,
424 virtual: synonym('abstract'),
425 yield: synonym('yields'),
426 /**
427 * Parse tag
428 * @private
429 * @param {Object} result target comment
430 * @param {Object} tag the tag
431 * @returns {undefined} has side-effects
432 */
433 yields(result, tag) {
434 const yields = {
435 description: parseMarkdown(tag.description),
436 title: 'yields'
437 };
438
439 if (tag.type) {
440 yields.type = tag.type;
441 }
442
443 result.yields.push(yields);
444 }
445};
446
447/**
448 * A no-op function for unsupported tags
449 * @returns {undefined} does nothing
450 */
451function todo() {}
452
453/**
454 * Generate a function that curries a destination key for a flattener
455 * @private
456 * @param {string} key the eventual destination key
457 * @returns {Function} a flattener that remembers that key
458 */
459function synonym(key) {
460 return function(result, tag) {
461 const fun = flatteners[key];
462 fun.apply(null, [result, tag, key].slice(0, fun.length));
463 };
464}
465
466/**
467 * Treat the existence of a tag as a sign to mark `key` as true in the result
468 * @private
469 * @param {Object} result the documentation object
470 * @param {Object} tag the tag object, with a name property
471 * @param {string} key destination on the result
472 * @returns {undefined} operates with side-effects
473 */
474function flattenBoolean(result, tag, key) {
475 result[key] = true;
476}
477
478/**
479 * Flatten a usable-once name tag into a key
480 * @private
481 * @param {Object} result the documentation object
482 * @param {Object} tag the tag object, with a name property
483 * @param {string} key destination on the result
484 * @returns {undefined} operates with side-effects
485 */
486function flattenName(result, tag, key) {
487 result[key] = tag.name;
488}
489
490/**
491 * Flatten a usable-once description tag into a key
492 * @private
493 * @param {Object} result the documentation object
494 * @param {Object} tag the tag object, with a description property
495 * @param {string} key destination on the result
496 * @returns {undefined} operates with side-effects
497 */
498function flattenDescription(result, tag, key) {
499 result[key] = tag.description;
500}
501
502/**
503 * Flatten a usable-once description tag into a key and parse it as Markdown
504 * @private
505 * @param {Object} result the documentation object
506 * @param {Object} tag the tag object, with a description property
507 * @param {string} key destination on the result
508 * @returns {undefined} operates with side-effects
509 */
510function flattenMarkdownDescription(result, tag, key) {
511 result[key] = parseMarkdown(tag.description);
512}
513
514/**
515 * Parse [kind shorthand](http://usejsdoc.org/tags-kind.html) into
516 * both name and type tags, like `@class [<type> <name>]`
517 *
518 * @param {Object} result comment
519 * @param {Object} tag parsed tag
520 * @param {string} key tag
521 * @returns {undefined} operates through side effects
522 * @private
523 */
524function flattenKindShorthand(result, tag, key) {
525 result.kind = key;
526
527 if (tag.name) {
528 result.name = tag.name;
529 }
530
531 if (tag.type) {
532 result.type = tag.type;
533 }
534}
535
536/**
537 * Parse a comment with doctrine, decorate the result with file position and code
538 * context, handle parsing errors, and fix up various infelicities in the structure
539 * outputted by doctrine.
540 *
541 * The following tags are treated as synonyms for a canonical tag:
542 *
543 * * `@virtual` ⇢ `@abstract`
544 * * `@extends` ⇢ `@augments`
545 * * `@constructor` ⇢ `@class`
546 * * `@const` ⇢ `@constant`
547 * * `@defaultvalue` ⇢ `@default`
548 * * `@desc` ⇢ `@description`
549 * * `@host` ⇢ `@external`
550 * * `@fileoverview`, `@overview` ⇢ `@file`
551 * * `@emits` ⇢ `@fires`
552 * * `@func`, `@method` ⇢ `@function`
553 * * `@var` ⇢ `@member`
554 * * `@arg`, `@argument` ⇢ `@param`
555 * * `@prop` ⇢ `@property`
556 * * `@return` ⇢ `@returns`
557 * * `@exception` ⇢ `@throws`
558 * * `@linkcode`, `@linkplain` ⇢ `@link`
559 *
560 * The following tags are assumed to be singletons, and are flattened
561 * to a top-level property on the result whose value is extracted from
562 * the tag:
563 *
564 * * `@name`
565 * * `@memberof`
566 * * `@classdesc`
567 * * `@kind`
568 * * `@class`
569 * * `@constant`
570 * * `@event`
571 * * `@external`
572 * * `@file`
573 * * `@function`
574 * * `@member`
575 * * `@mixin`
576 * * `@module`
577 * * `@namespace`
578 * * `@typedef`
579 * * `@access`
580 * * `@lends`
581 * * `@description`
582 * * `@summary`
583 * * `@copyright`
584 * * `@deprecated`
585 *
586 * The following tags are flattened to a top-level array-valued property:
587 *
588 * * `@param` (to `params` property)
589 * * `@property` (to `properties` property)
590 * * `@returns` (to `returns` property)
591 * * `@augments` (to `augments` property)
592 * * `@example` (to `examples` property)
593 * * `@throws` (to `throws` property)
594 * * `@see` (to `sees` property)
595 * * `@todo` (to `todos` property)
596 *
597 * The `@global`, `@static`, `@instance`, and `@inner` tags are flattened
598 * to a `scope` property whose value is `"global"`, `"static"`, `"instance"`,
599 * or `"inner"`.
600 *
601 * The `@access`, `@public`, `@protected`, and `@private` tags are flattened
602 * to an `access` property whose value is `"protected"` or `"private"`.
603 * The assumed default value is `"public"`, so `@access public` or `@public`
604 * tags result in no `access` property.
605 *
606 * @param {string} comment input to be parsed
607 * @param {Object} loc location of the input
608 * @param {Object} context code context of the input
609 * @returns {Comment} an object conforming to the
610 * [documentation schema](https://github.com/documentationjs/api-json)
611 */
612function parseJSDoc(comment, loc, context) {
613 const result = doctrine.parse(comment, {
614 // have doctrine itself remove the comment asterisks from content
615 unwrap: true,
616 // enable parsing of optional parameters in brackets, JSDoc3 style
617 sloppy: true,
618 // `recoverable: true` is the only way to get error information out
619 recoverable: true,
620 // include line numbers
621 lineNumbers: true
622 });
623
624 result.loc = loc;
625 result.context = context;
626
627 result.augments = [];
628 result.errors = [];
629 result.examples = [];
630 result.implements = [];
631 result.params = [];
632 result.properties = [];
633 result.returns = [];
634 result.sees = [];
635 result.throws = [];
636 result.todos = [];
637 result.yields = [];
638
639 if (result.description) {
640 result.description = parseMarkdown(result.description);
641 }
642
643 // Reject parameter tags without a parameter name
644 result.tags.filter(function(tag) {
645 if (tag.title === 'param' && tag.name === undefined) {
646 result.errors.push({
647 message: 'A @param tag without a parameter name was rejected'
648 });
649 return false;
650 }
651 return true;
652 });
653
654 result.tags.forEach(function(tag) {
655 if (tag.errors) {
656 for (let j = 0; j < tag.errors.length; j++) {
657 result.errors.push({ message: tag.errors[j] });
658 }
659 } else if (flatteners[tag.title]) {
660 flatteners[tag.title](result, tag, tag.title);
661 } else {
662 result.errors.push({
663 message: 'unknown tag @' + tag.title,
664 commentLineNumber: tag.lineNumber
665 });
666 }
667 });
668
669 // Using the @name tag, or any other tag that sets the name of a comment,
670 // disconnects the comment from its surrounding code.
671 if (context && result.name) {
672 delete context.ast;
673 }
674
675 return result;
676}
677
678module.exports = parseJSDoc;