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