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