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