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