• 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        "listens",
48        "member",
49        "memberof",
50        "method",
51        "mixes",
52        "module",
53        "name",
54        "namespace",
55        "override",
56        "package",
57        "param",
58        "private",
59        "property",
60        "protected",
61        "public",
62        "readonly",
63        "requires",
64        "returns",
65        "see",
66        "since",
67        "static",
68        "summary",
69        "template",
70        "this",
71        "throws",
72        "todo",
73        "tutorial",
74        "type",
75        "typedef",
76        "var",
77        "variation",
78        "version",
79        "virtual",
80        "yields"
81    ];
82    let jsDocTagNameCompletionEntries: CompletionEntry[];
83    let jsDocTagCompletionEntries: CompletionEntry[];
84
85    export function getJsDocCommentsFromDeclarations(declarations: readonly Declaration[]): SymbolDisplayPart[] {
86        // Only collect doc comments from duplicate declarations once:
87        // In case of a union property there might be same declaration multiple times
88        // which only varies in type parameter
89        // Eg. const a: Array<string> | Array<number>; a.length
90        // The property length will have two declarations of property length coming
91        // from Array<T> - Array<string> and Array<number>
92        const documentationComment: string[] = [];
93        forEachUnique(declarations, declaration => {
94            for (const { comment } of getCommentHavingNodes(declaration)) {
95                if (comment === undefined) continue;
96                pushIfUnique(documentationComment, comment);
97            }
98        });
99        return intersperse(map(documentationComment, textPart), lineBreakPart());
100    }
101
102    function getCommentHavingNodes(declaration: Declaration): readonly (JSDoc | JSDocTag)[] {
103        switch (declaration.kind) {
104            case SyntaxKind.JSDocParameterTag:
105            case SyntaxKind.JSDocPropertyTag:
106                return [declaration as JSDocPropertyTag];
107            case SyntaxKind.JSDocCallbackTag:
108            case SyntaxKind.JSDocTypedefTag:
109                return [(declaration as JSDocTypedefTag), (declaration as JSDocTypedefTag).parent];
110            default:
111                return getJSDocCommentsAndTags(declaration);
112        }
113    }
114
115    export function getJsDocTagsFromDeclarations(declarations?: Declaration[]): JSDocTagInfo[] {
116        // Only collect doc comments from duplicate declarations once.
117        const tags: JSDocTagInfo[] = [];
118        forEachUnique(declarations, declaration => {
119            for (const tag of getJSDocTags(declaration)) {
120                tags.push({ name: tag.tagName.text, text: getCommentText(tag) });
121            }
122        });
123        return tags;
124    }
125
126    function getCommentText(tag: JSDocTag): string | undefined {
127        const { comment } = tag;
128        switch (tag.kind) {
129            case SyntaxKind.JSDocImplementsTag:
130                return withNode((tag as JSDocImplementsTag).class);
131            case SyntaxKind.JSDocAugmentsTag:
132                return withNode((tag as JSDocAugmentsTag).class);
133            case SyntaxKind.JSDocTemplateTag:
134                return withList((tag as JSDocTemplateTag).typeParameters);
135            case SyntaxKind.JSDocTypeTag:
136                return withNode((tag as JSDocTypeTag).typeExpression);
137            case SyntaxKind.JSDocTypedefTag:
138            case SyntaxKind.JSDocCallbackTag:
139            case SyntaxKind.JSDocPropertyTag:
140            case SyntaxKind.JSDocParameterTag:
141            case SyntaxKind.JSDocSeeTag:
142                const { name } = tag as JSDocTypedefTag | JSDocPropertyTag | JSDocParameterTag | JSDocSeeTag;
143                return name ? withNode(name) : comment;
144            default:
145                return comment;
146        }
147
148        function withNode(node: Node) {
149            return addComment(node.getText());
150        }
151
152        function withList(list: NodeArray<Node>): string {
153            return addComment(list.map(x => x.getText()).join(", "));
154        }
155
156        function addComment(s: string) {
157            return comment === undefined ? s : `${s} ${comment}`;
158        }
159    }
160
161    export function getJSDocTagNameCompletions(): CompletionEntry[] {
162        return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = map(jsDocTagNames, tagName => {
163            return {
164                name: tagName,
165                kind: ScriptElementKind.keyword,
166                kindModifiers: "",
167                sortText: Completions.SortText.LocationPriority,
168            };
169        }));
170    }
171
172    export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails;
173
174    export function getJSDocTagCompletions(): CompletionEntry[] {
175        return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = map(jsDocTagNames, tagName => {
176            return {
177                name: `@${tagName}`,
178                kind: ScriptElementKind.keyword,
179                kindModifiers: "",
180                sortText: Completions.SortText.LocationPriority
181            };
182        }));
183    }
184
185    export function getJSDocTagCompletionDetails(name: string): CompletionEntryDetails {
186        return {
187            name,
188            kind: ScriptElementKind.unknown, // TODO: should have its own kind?
189            kindModifiers: "",
190            displayParts: [textPart(name)],
191            documentation: emptyArray,
192            tags: undefined,
193            codeActions: undefined,
194        };
195    }
196
197    export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] {
198        if (!isIdentifier(tag.name)) {
199            return emptyArray;
200        }
201        const nameThusFar = tag.name.text;
202        const jsdoc = tag.parent;
203        const fn = jsdoc.parent;
204        if (!isFunctionLike(fn)) return [];
205
206        return mapDefined(fn.parameters, param => {
207            if (!isIdentifier(param.name)) return undefined;
208
209            const name = param.name.text;
210            if (jsdoc.tags!.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.name) && t.name.escapedText === name) // TODO: GH#18217
211                || nameThusFar !== undefined && !startsWith(name, nameThusFar)) {
212                return undefined;
213            }
214
215            return { name, kind: ScriptElementKind.parameterElement, kindModifiers: "", sortText: Completions.SortText.LocationPriority };
216        });
217    }
218
219    export function getJSDocParameterNameCompletionDetails(name: string): CompletionEntryDetails {
220        return {
221            name,
222            kind: ScriptElementKind.parameterElement,
223            kindModifiers: "",
224            displayParts: [textPart(name)],
225            documentation: emptyArray,
226            tags: undefined,
227            codeActions: undefined,
228        };
229    }
230
231    /**
232     * Checks if position points to a valid position to add JSDoc comments, and if so,
233     * returns the appropriate template. Otherwise returns an empty string.
234     * Valid positions are
235     *      - outside of comments, statements, and expressions, and
236     *      - preceding a:
237     *          - function/constructor/method declaration
238     *          - class declarations
239     *          - variable statements
240     *          - namespace declarations
241     *          - interface declarations
242     *          - method signatures
243     *          - type alias declarations
244     *
245     * Hosts should ideally check that:
246     * - The line is all whitespace up to 'position' before performing the insertion.
247     * - If the keystroke sequence "/\*\*" induced the call, we also check that the next
248     * non-whitespace character is '*', which (approximately) indicates whether we added
249     * the second '*' to complete an existing (JSDoc) comment.
250     * @param fileName The file in which to perform the check.
251     * @param position The (character-indexed) position in the file where the check should
252     * be performed.
253     */
254    export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number, options?: DocCommentTemplateOptions): TextInsertion | undefined {
255        const tokenAtPos = getTokenAtPosition(sourceFile, position);
256        const existingDocComment = findAncestor(tokenAtPos, isJSDoc);
257        if (existingDocComment && (existingDocComment.comment !== undefined || length(existingDocComment.tags))) {
258            // Non-empty comment already exists.
259            return undefined;
260        }
261
262        const tokenStart = tokenAtPos.getStart(sourceFile);
263        // Don't provide a doc comment template based on a *previous* node. (But an existing empty jsdoc comment will likely start before `position`.)
264        if (!existingDocComment && tokenStart < position) {
265            return undefined;
266        }
267
268        const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos, options);
269        if (!commentOwnerInfo) {
270            return undefined;
271        }
272
273        const { commentOwner, parameters, hasReturn } = commentOwnerInfo;
274        if (commentOwner.getStart(sourceFile) < position) {
275            return undefined;
276        }
277
278        const indentationStr = getIndentationStringAtPosition(sourceFile, position);
279        const isJavaScriptFile = hasJSFileExtension(sourceFile.fileName);
280        const tags =
281            (parameters ? parameterDocComments(parameters || [], isJavaScriptFile, indentationStr, newLine) : "") +
282            (hasReturn ? returnsDocComment(indentationStr, newLine) : "");
283
284        // A doc comment consists of the following
285        // * The opening comment line
286        // * the first line (without a param) for the object's untagged info (this is also where the caret ends up)
287        // * the '@param'-tagged lines
288        // * the '@returns'-tag
289        // * TODO: other tags.
290        // * the closing comment line
291        // * if the caret was directly in front of the object, then we add an extra line and indentation.
292        const openComment = "/**";
293        const closeComment = " */";
294        if (tags) {
295            const preamble = openComment + newLine + indentationStr + " * ";
296            const endLine = tokenStart === position ? newLine + indentationStr : "";
297            const result = preamble + newLine + tags + indentationStr + closeComment + endLine;
298            return { newText: result, caretOffset: preamble.length };
299        }
300        return { newText: openComment + closeComment, caretOffset: 3 };
301    }
302
303    function getIndentationStringAtPosition(sourceFile: SourceFile, position: number): string {
304        const { text } = sourceFile;
305        const lineStart = getLineStartPositionForPosition(position, sourceFile);
306        let pos = lineStart;
307        for (; pos <= position && isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++);
308        return text.slice(lineStart, pos);
309    }
310
311    function parameterDocComments(parameters: readonly ParameterDeclaration[], isJavaScriptFile: boolean, indentationStr: string, newLine: string): string {
312        return parameters.map(({ name, dotDotDotToken }, i) => {
313            const paramName = name.kind === SyntaxKind.Identifier ? name.text : "param" + i;
314            const type = isJavaScriptFile ? (dotDotDotToken ? "{...any} " : "{any} ") : "";
315            return `${indentationStr} * @param ${type}${paramName}${newLine}`;
316        }).join("");
317    }
318
319    function returnsDocComment(indentationStr: string, newLine: string) {
320        return `${indentationStr} * @returns${newLine}`;
321    }
322
323    interface CommentOwnerInfo {
324        readonly commentOwner: Node;
325        readonly parameters?: readonly ParameterDeclaration[];
326        readonly hasReturn?: boolean;
327    }
328    function getCommentOwnerInfo(tokenAtPos: Node, options: DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined {
329        return forEachAncestor(tokenAtPos, n => getCommentOwnerInfoWorker(n, options));
330    }
331    function getCommentOwnerInfoWorker(commentOwner: Node, options: DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined | "quit" {
332        switch (commentOwner.kind) {
333            case SyntaxKind.FunctionDeclaration:
334            case SyntaxKind.FunctionExpression:
335            case SyntaxKind.MethodDeclaration:
336            case SyntaxKind.Constructor:
337            case SyntaxKind.MethodSignature:
338            case SyntaxKind.ArrowFunction:
339                const host = commentOwner as ArrowFunction | FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature;
340                return { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) };
341
342            case SyntaxKind.PropertyAssignment:
343                return getCommentOwnerInfoWorker((commentOwner as PropertyAssignment).initializer, options);
344
345            case SyntaxKind.ClassDeclaration:
346            case SyntaxKind.StructDeclaration:
347            case SyntaxKind.InterfaceDeclaration:
348            case SyntaxKind.PropertySignature:
349            case SyntaxKind.EnumDeclaration:
350            case SyntaxKind.EnumMember:
351            case SyntaxKind.TypeAliasDeclaration:
352                return { commentOwner };
353
354            case SyntaxKind.VariableStatement: {
355                const varStatement = <VariableStatement>commentOwner;
356                const varDeclarations = varStatement.declarationList.declarations;
357                const host = varDeclarations.length === 1 && varDeclarations[0].initializer
358                    ? getRightHandSideOfAssignment(varDeclarations[0].initializer)
359                    : undefined;
360                return host
361                    ? { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) }
362                    : { commentOwner };
363            }
364
365            case SyntaxKind.SourceFile:
366                return "quit";
367
368            case SyntaxKind.ModuleDeclaration:
369                // If in walking up the tree, we hit a a nested namespace declaration,
370                // then we must be somewhere within a dotted namespace name; however we don't
371                // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'.
372                return commentOwner.parent.kind === SyntaxKind.ModuleDeclaration ? undefined : { commentOwner };
373
374            case SyntaxKind.ExpressionStatement:
375                return getCommentOwnerInfoWorker((commentOwner as ExpressionStatement).expression, options);
376            case SyntaxKind.BinaryExpression: {
377                const be = commentOwner as BinaryExpression;
378                if (getAssignmentDeclarationKind(be) === AssignmentDeclarationKind.None) {
379                    return "quit";
380                }
381                return isFunctionLike(be.right)
382                    ? { commentOwner, parameters: be.right.parameters, hasReturn: hasReturn(be.right, options) }
383                    : { commentOwner };
384            }
385            case SyntaxKind.PropertyDeclaration:
386                const init = (commentOwner as PropertyDeclaration).initializer;
387                if (init && (isFunctionExpression(init) || isArrowFunction(init))) {
388                    return { commentOwner, parameters: init.parameters, hasReturn: hasReturn(init, options) };
389                }
390        }
391    }
392
393    function hasReturn(node: Node, options: DocCommentTemplateOptions | undefined) {
394        return !!options?.generateReturnInDocTemplate &&
395            (isArrowFunction(node) && isExpression(node.body)
396                || isFunctionLikeDeclaration(node) && node.body && isBlock(node.body) && !!forEachReturnStatement(node.body, n => n));
397    }
398
399    function getRightHandSideOfAssignment(rightHandSide: Expression): FunctionExpression | ArrowFunction | ConstructorDeclaration | undefined {
400        while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) {
401            rightHandSide = (<ParenthesizedExpression>rightHandSide).expression;
402        }
403
404        switch (rightHandSide.kind) {
405            case SyntaxKind.FunctionExpression:
406            case SyntaxKind.ArrowFunction:
407                return (<FunctionExpression>rightHandSide);
408            case SyntaxKind.ClassExpression:
409                return find((rightHandSide as ClassExpression).members, isConstructorDeclaration);
410        }
411    }
412}
413