1 | const doctrine = require('doctrine-temporary-fork');
|
2 | const 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 | */
|
10 | const 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 | */
|
451 | function 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 | */
|
459 | function 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 | */
|
474 | function 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 | */
|
486 | function 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 | */
|
498 | function 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 | */
|
510 | function 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 | */
|
524 | function 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 | */
|
612 | function 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 |
|
678 | module.exports = parseJSDoc;
|