• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.JsDoc {
3    const jsDocTagNames = [
4        "abstract",
5        "access",
6        "alias",
7        "argument",
8        "async",
9        "augments",
10        "author",
11        "borrows",
12        "callback",
13        "class",
14        "classdesc",
15        "constant",
16        "constructor",
17        "constructs",
18        "copyright",
19        "default",
20        "deprecated",
21        "description",
22        "emits",
23        "enum",
24        "event",
25        "example",
26        "exports",
27        "extends",
28        "external",
29        "field",
30        "file",
31        "fileoverview",
32        "fires",
33        "function",
34        "generator",
35        "global",
36        "hideconstructor",
37        "host",
38        "ignore",
39        "implements",
40        "inheritdoc",
41        "inner",
42        "instance",
43        "interface",
44        "kind",
45        "lends",
46        "license",
47        "link",
48        "listens",
49        "member",
50        "memberof",
51        "method",
52        "mixes",
53        "module",
54        "name",
55        "namespace",
56        "override",
57        "package",
58        "param",
59        "private",
60        "property",
61        "protected",
62        "public",
63        "readonly",
64        "requires",
65        "returns",
66        "see",
67        "since",
68        "static",
69        "summary",
70        "template",
71        "this",
72        "throws",
73        "todo",
74        "tutorial",
75        "type",
76        "typedef",
77        "var",
78        "variation",
79        "version",
80        "virtual",
81        "yields"
82    ];
83    let jsDocTagNameCompletionEntries: CompletionEntry[];
84    let jsDocTagCompletionEntries: CompletionEntry[];
85
86    export function getJsDocCommentsFromDeclarations(declarations: readonly Declaration[], checker?: TypeChecker): SymbolDisplayPart[] {
87        // Only collect doc comments from duplicate declarations once:
88        // In case of a union property there might be same declaration multiple times
89        // which only varies in type parameter
90        // Eg. const a: Array<string> | Array<number>; a.length
91        // The property length will have two declarations of property length coming
92        // from Array<T> - Array<string> and Array<number>
93        const parts: SymbolDisplayPart[][] = [];
94        forEachUnique(declarations, declaration => {
95            for (const jsdoc of getCommentHavingNodes(declaration)) {
96                const inheritDoc = isJSDoc(jsdoc) && jsdoc.tags && find(jsdoc.tags, t => t.kind === SyntaxKind.JSDocTag && (t.tagName.escapedText === "inheritDoc" || t.tagName.escapedText === "inheritdoc"));
97                // skip comments containing @typedefs since they're not associated with particular declarations
98                // Exceptions:
99                // - @typedefs are themselves declarations with associated comments
100                // - @param or @return indicate that the author thinks of it as a 'local' @typedef that's part of the function documentation
101                if (jsdoc.comment === undefined && !inheritDoc
102                    || isJSDoc(jsdoc)
103                       && declaration.kind !== SyntaxKind.JSDocTypedefTag && declaration.kind !== SyntaxKind.JSDocCallbackTag
104                       && jsdoc.tags
105                       && jsdoc.tags.some(t => t.kind === SyntaxKind.JSDocTypedefTag || t.kind === SyntaxKind.JSDocCallbackTag)
106                       && !jsdoc.tags.some(t => t.kind === SyntaxKind.JSDocParameterTag || t.kind === SyntaxKind.JSDocReturnTag)) {
107                    continue;
108                }
109                let newparts = jsdoc.comment ? getDisplayPartsFromComment(jsdoc.comment, checker) : [];
110                if (inheritDoc && inheritDoc.comment) {
111                    newparts = newparts.concat(getDisplayPartsFromComment(inheritDoc.comment, checker));
112                }
113                if (!contains(parts, newparts, isIdenticalListOfDisplayParts)) {
114                    parts.push(newparts);
115                }
116            }
117        });
118        return flatten(intersperse(parts, [lineBreakPart()]));
119    }
120
121    function isIdenticalListOfDisplayParts(parts1: SymbolDisplayPart[], parts2: SymbolDisplayPart[]) {
122        return arraysEqual(parts1, parts2, (p1, p2) => p1.kind === p2.kind && p1.text === p2.text);
123    }
124
125    function getCommentHavingNodes(declaration: Declaration): readonly (JSDoc | JSDocTag)[] {
126        switch (declaration.kind) {
127            case SyntaxKind.JSDocParameterTag:
128            case SyntaxKind.JSDocPropertyTag:
129                return [declaration as JSDocPropertyTag];
130            case SyntaxKind.JSDocCallbackTag:
131            case SyntaxKind.JSDocTypedefTag:
132                return [(declaration as JSDocTypedefTag), (declaration as JSDocTypedefTag).parent];
133            default:
134                return getJSDocCommentsAndTags(declaration);
135        }
136    }
137
138    export function getJsDocTagsFromDeclarations(declarations?: Declaration[], checker?: TypeChecker): JSDocTagInfo[] {
139        // Only collect doc comments from duplicate declarations once.
140        const infos: JSDocTagInfo[] = [];
141        forEachUnique(declarations, declaration => {
142            const tags = getJSDocTags(declaration);
143            // skip comments containing @typedefs since they're not associated with particular declarations
144            // Exceptions:
145            // - @param or @return indicate that the author thinks of it as a 'local' @typedef that's part of the function documentation
146            if (tags.some(t => t.kind === SyntaxKind.JSDocTypedefTag || t.kind === SyntaxKind.JSDocCallbackTag)
147                && !tags.some(t => t.kind === SyntaxKind.JSDocParameterTag || t.kind === SyntaxKind.JSDocReturnTag)) {
148                return;
149            }
150            for (const tag of tags) {
151                infos.push({ name: tag.tagName.text, text: getCommentDisplayParts(tag, checker) });
152            }
153        });
154        return infos;
155    }
156
157    function getDisplayPartsFromComment(comment: string | readonly JSDocComment[], checker: TypeChecker | undefined): SymbolDisplayPart[] {
158        if (typeof comment === "string") {
159            return [textPart(comment)];
160        }
161        return flatMap(
162            comment,
163            node => node.kind === SyntaxKind.JSDocText ? [textPart(node.text)] : buildLinkParts(node, checker)
164        ) as SymbolDisplayPart[];
165    }
166
167    function getCommentDisplayParts(tag: JSDocTag, checker?: TypeChecker): SymbolDisplayPart[] | undefined {
168        const { comment, kind } = tag;
169        const namePart = getTagNameDisplayPart(kind);
170        switch (kind) {
171            case SyntaxKind.JSDocImplementsTag:
172                return withNode((tag as JSDocImplementsTag).class);
173            case SyntaxKind.JSDocAugmentsTag:
174                return withNode((tag as JSDocAugmentsTag).class);
175            case SyntaxKind.JSDocTemplateTag:
176                const templateTag = tag as JSDocTemplateTag;
177                const displayParts: SymbolDisplayPart[] = [];
178                if (templateTag.constraint) {
179                    displayParts.push(textPart(templateTag.constraint.getText()));
180                }
181                if (length(templateTag.typeParameters)) {
182                    if (length(displayParts)) {
183                        displayParts.push(spacePart());
184                    }
185                    const lastTypeParameter = templateTag.typeParameters[templateTag.typeParameters.length - 1];
186                    forEach(templateTag.typeParameters, tp => {
187                        displayParts.push(namePart(tp.getText()));
188                        if (lastTypeParameter !== tp) {
189                            displayParts.push(...[punctuationPart(SyntaxKind.CommaToken), spacePart()]);
190                        }
191                    });
192                }
193                if (comment) {
194                    displayParts.push(...[spacePart(), ...getDisplayPartsFromComment(comment, checker)]);
195                }
196                return displayParts;
197            case SyntaxKind.JSDocTypeTag:
198                return withNode((tag as JSDocTypeTag).typeExpression);
199            case SyntaxKind.JSDocTypedefTag:
200            case SyntaxKind.JSDocCallbackTag:
201            case SyntaxKind.JSDocPropertyTag:
202            case SyntaxKind.JSDocParameterTag:
203            case SyntaxKind.JSDocSeeTag:
204                const { name } = tag as JSDocTypedefTag | JSDocCallbackTag | JSDocPropertyTag | JSDocParameterTag | JSDocSeeTag;
205                return name ? withNode(name)
206                    : comment === undefined ? undefined
207                    : getDisplayPartsFromComment(comment, checker);
208            default:
209                return comment === undefined ? undefined : getDisplayPartsFromComment(comment, checker);
210        }
211
212        function withNode(node: Node) {
213            return addComment(node.getText());
214        }
215
216        function addComment(s: string) {
217            if (comment) {
218                if (s.match(/^https?$/)) {
219                    return [textPart(s), ...getDisplayPartsFromComment(comment, checker)];
220                }
221                else {
222                    return [namePart(s), spacePart(), ...getDisplayPartsFromComment(comment, checker)];
223                }
224            }
225            else {
226                return [textPart(s)];
227            }
228        }
229    }
230
231    function getTagNameDisplayPart(kind: SyntaxKind): (text: string) => SymbolDisplayPart {
232        switch (kind) {
233            case SyntaxKind.JSDocParameterTag:
234                return parameterNamePart;
235            case SyntaxKind.JSDocPropertyTag:
236                return propertyNamePart;
237            case SyntaxKind.JSDocTemplateTag:
238                return typeParameterNamePart;
239            case SyntaxKind.JSDocTypedefTag:
240            case SyntaxKind.JSDocCallbackTag:
241                return typeAliasNamePart;
242            default:
243                return textPart;
244        }
245    }
246
247    export function getJSDocTagNameCompletions(): CompletionEntry[] {
248        return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = map(jsDocTagNames, tagName => {
249            return {
250                name: tagName,
251                kind: ScriptElementKind.keyword,
252                kindModifiers: "",
253                sortText: Completions.SortText.LocationPriority,
254            };
255        }));
256    }
257
258    export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails;
259
260    export function getJSDocTagCompletions(): CompletionEntry[] {
261        return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = map(jsDocTagNames, tagName => {
262            return {
263                name: `@${tagName}`,
264                kind: ScriptElementKind.keyword,
265                kindModifiers: "",
266                sortText: Completions.SortText.LocationPriority
267            };
268        }));
269    }
270
271    export function getJSDocTagCompletionDetails(name: string): CompletionEntryDetails {
272        return {
273            name,
274            kind: ScriptElementKind.unknown, // TODO: should have its own kind?
275            kindModifiers: "",
276            displayParts: [textPart(name)],
277            documentation: emptyArray,
278            tags: undefined,
279            codeActions: undefined,
280        };
281    }
282
283    export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] {
284        if (!isIdentifier(tag.name)) {
285            return emptyArray;
286        }
287        const nameThusFar = tag.name.text;
288        const jsdoc = tag.parent;
289        const fn = jsdoc.parent;
290        if (!isFunctionLike(fn)) return [];
291
292        return mapDefined(fn.parameters, param => {
293            if (!isIdentifier(param.name)) return undefined;
294
295            const name = param.name.text;
296            if (jsdoc.tags!.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.name) && t.name.escapedText === name) // TODO: GH#18217
297                || nameThusFar !== undefined && !startsWith(name, nameThusFar)) {
298                return undefined;
299            }
300
301            return { name, kind: ScriptElementKind.parameterElement, kindModifiers: "", sortText: Completions.SortText.LocationPriority };
302        });
303    }
304
305    export function getJSDocParameterNameCompletionDetails(name: string): CompletionEntryDetails {
306        return {
307            name,
308            kind: ScriptElementKind.parameterElement,
309            kindModifiers: "",
310            displayParts: [textPart(name)],
311            documentation: emptyArray,
312            tags: undefined,
313            codeActions: undefined,
314        };
315    }
316
317    /**
318     * Checks if position points to a valid position to add JSDoc comments, and if so,
319     * returns the appropriate template. Otherwise returns an empty string.
320     * Valid positions are
321     *      - outside of comments, statements, and expressions, and
322     *      - preceding a:
323     *          - function/constructor/method declaration
324     *          - class declarations
325     *          - variable statements
326     *          - namespace declarations
327     *          - interface declarations
328     *          - method signatures
329     *          - type alias declarations
330     *
331     * Hosts should ideally check that:
332     * - The line is all whitespace up to 'position' before performing the insertion.
333     * - If the keystroke sequence "/\*\*" induced the call, we also check that the next
334     * non-whitespace character is '*', which (approximately) indicates whether we added
335     * the second '*' to complete an existing (JSDoc) comment.
336     * @param fileName The file in which to perform the check.
337     * @param position The (character-indexed) position in the file where the check should
338     * be performed.
339     */
340    export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number, options?: DocCommentTemplateOptions): TextInsertion | undefined {
341        const tokenAtPos = getTokenAtPosition(sourceFile, position);
342        const existingDocComment = findAncestor(tokenAtPos, isJSDoc);
343        if (existingDocComment && (existingDocComment.comment !== undefined || length(existingDocComment.tags))) {
344            // Non-empty comment already exists.
345            return undefined;
346        }
347
348        const tokenStart = tokenAtPos.getStart(sourceFile);
349        // Don't provide a doc comment template based on a *previous* node. (But an existing empty jsdoc comment will likely start before `position`.)
350        if (!existingDocComment && tokenStart < position) {
351            return undefined;
352        }
353
354        const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos, options);
355        if (!commentOwnerInfo) {
356            return undefined;
357        }
358
359        const { commentOwner, parameters, hasReturn } = commentOwnerInfo;
360        const commentOwnerJsDoc = hasJSDocNodes(commentOwner) && commentOwner.jsDoc ? commentOwner.jsDoc : undefined;
361        const lastJsDoc = lastOrUndefined(commentOwnerJsDoc);
362        if (commentOwner.getStart(sourceFile) < position
363            || lastJsDoc
364                && existingDocComment
365                && lastJsDoc !== existingDocComment) {
366            return undefined;
367        }
368
369        const indentationStr = getIndentationStringAtPosition(sourceFile, position);
370        const isJavaScriptFile = hasJSFileExtension(sourceFile.fileName);
371        const tags =
372            (parameters ? parameterDocComments(parameters || [], isJavaScriptFile, indentationStr, newLine) : "") +
373            (hasReturn ? returnsDocComment(indentationStr, newLine) : "");
374
375        // A doc comment consists of the following
376        // * The opening comment line
377        // * the first line (without a param) for the object's untagged info (this is also where the caret ends up)
378        // * the '@param'-tagged lines
379        // * the '@returns'-tag
380        // * TODO: other tags.
381        // * the closing comment line
382        // * if the caret was directly in front of the object, then we add an extra line and indentation.
383        const openComment = "/**";
384        const closeComment = " */";
385
386        // If any of the existing jsDoc has tags, ignore adding new ones.
387        const hasTag = (commentOwnerJsDoc || []).some(jsDoc => !!jsDoc.tags);
388
389        if (tags && !hasTag) {
390            const preamble = openComment + newLine + indentationStr + " * ";
391            const endLine = tokenStart === position ? newLine + indentationStr : "";
392            const result = preamble + newLine + tags + indentationStr + closeComment + endLine;
393            return { newText: result, caretOffset: preamble.length };
394        }
395        return { newText: openComment + closeComment, caretOffset: 3 };
396    }
397
398    function getIndentationStringAtPosition(sourceFile: SourceFile, position: number): string {
399        const { text } = sourceFile;
400        const lineStart = getLineStartPositionForPosition(position, sourceFile);
401        let pos = lineStart;
402        for (; pos <= position && isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++);
403        return text.slice(lineStart, pos);
404    }
405
406    function parameterDocComments(parameters: readonly ParameterDeclaration[], isJavaScriptFile: boolean, indentationStr: string, newLine: string): string {
407        return parameters.map(({ name, dotDotDotToken }, i) => {
408            const paramName = name.kind === SyntaxKind.Identifier ? name.text : "param" + i;
409            const type = isJavaScriptFile ? (dotDotDotToken ? "{...any} " : "{any} ") : "";
410            return `${indentationStr} * @param ${type}${paramName}${newLine}`;
411        }).join("");
412    }
413
414    function returnsDocComment(indentationStr: string, newLine: string) {
415        return `${indentationStr} * @returns${newLine}`;
416    }
417
418    interface CommentOwnerInfo {
419        readonly commentOwner: Node;
420        readonly parameters?: readonly ParameterDeclaration[];
421        readonly hasReturn?: boolean;
422    }
423    function getCommentOwnerInfo(tokenAtPos: Node, options: DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined {
424        return forEachAncestor(tokenAtPos, n => getCommentOwnerInfoWorker(n, options));
425    }
426    function getCommentOwnerInfoWorker(commentOwner: Node, options: DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined | "quit" {
427        switch (commentOwner.kind) {
428            case SyntaxKind.FunctionDeclaration:
429            case SyntaxKind.FunctionExpression:
430            case SyntaxKind.MethodDeclaration:
431            case SyntaxKind.Constructor:
432            case SyntaxKind.MethodSignature:
433            case SyntaxKind.ArrowFunction:
434                const host = commentOwner as ArrowFunction | FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature;
435                return { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) };
436
437            case SyntaxKind.PropertyAssignment:
438                return getCommentOwnerInfoWorker((commentOwner as PropertyAssignment).initializer, options);
439
440            case SyntaxKind.ClassDeclaration:
441            case SyntaxKind.StructDeclaration:
442            case SyntaxKind.InterfaceDeclaration:
443            case SyntaxKind.EnumDeclaration:
444            case SyntaxKind.EnumMember:
445            case SyntaxKind.TypeAliasDeclaration:
446                return { commentOwner };
447
448            case SyntaxKind.PropertySignature: {
449                const host = commentOwner as PropertySignature;
450                return host.type && isFunctionTypeNode(host.type)
451                    ? { commentOwner, parameters: host.type.parameters, hasReturn: hasReturn(host.type, options) }
452                    : { commentOwner };
453            }
454
455            case SyntaxKind.VariableStatement: {
456                const varStatement = commentOwner as VariableStatement;
457                const varDeclarations = varStatement.declarationList.declarations;
458                const host = varDeclarations.length === 1 && varDeclarations[0].initializer
459                    ? getRightHandSideOfAssignment(varDeclarations[0].initializer)
460                    : undefined;
461                return host
462                    ? { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) }
463                    : { commentOwner };
464            }
465
466            case SyntaxKind.SourceFile:
467                return "quit";
468
469            case SyntaxKind.ModuleDeclaration:
470                // If in walking up the tree, we hit a a nested namespace declaration,
471                // then we must be somewhere within a dotted namespace name; however we don't
472                // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'.
473                return commentOwner.parent.kind === SyntaxKind.ModuleDeclaration ? undefined : { commentOwner };
474
475            case SyntaxKind.ExpressionStatement:
476                return getCommentOwnerInfoWorker((commentOwner as ExpressionStatement).expression, options);
477            case SyntaxKind.BinaryExpression: {
478                const be = commentOwner as BinaryExpression;
479                if (getAssignmentDeclarationKind(be) === AssignmentDeclarationKind.None) {
480                    return "quit";
481                }
482                return isFunctionLike(be.right)
483                    ? { commentOwner, parameters: be.right.parameters, hasReturn: hasReturn(be.right, options) }
484                    : { commentOwner };
485            }
486            case SyntaxKind.PropertyDeclaration:
487                const init = (commentOwner as PropertyDeclaration).initializer;
488                if (init && (isFunctionExpression(init) || isArrowFunction(init))) {
489                    return { commentOwner, parameters: init.parameters, hasReturn: hasReturn(init, options) };
490                }
491        }
492    }
493
494    function hasReturn(node: Node, options: DocCommentTemplateOptions | undefined) {
495        return !!options?.generateReturnInDocTemplate &&
496            (isFunctionTypeNode(node) || isArrowFunction(node) && isExpression(node.body)
497                || isFunctionLikeDeclaration(node) && node.body && isBlock(node.body) && !!forEachReturnStatement(node.body, n => n));
498    }
499
500    function getRightHandSideOfAssignment(rightHandSide: Expression): FunctionExpression | ArrowFunction | ConstructorDeclaration | undefined {
501        while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) {
502            rightHandSide = (rightHandSide as ParenthesizedExpression).expression;
503        }
504
505        switch (rightHandSide.kind) {
506            case SyntaxKind.FunctionExpression:
507            case SyntaxKind.ArrowFunction:
508                return (rightHandSide as FunctionExpression);
509            case SyntaxKind.ClassExpression:
510                return find((rightHandSide as ClassExpression).members, isConstructorDeclaration);
511        }
512    }
513}
514