/* @internal */ namespace ts.JsDoc { const jsDocTagNames = [ "augments", "author", "argument", "borrows", "class", "constant", "constructor", "constructs", "default", "deprecated", "description", "event", "example", "extends", "field", "fileOverview", "function", "ignore", "inner", "lends", "link", "memberOf", "name", "namespace", "param", "private", "property", "public", "requires", "returns", "see", "since", "static", "throws", "type", "typedef", "property", "prop", "version" ]; let jsDocCompletionEntries: CompletionEntry[]; export function getJsDocCommentsFromDeclarations(declarations: Declaration[]) { // Only collect doc comments from duplicate declarations once: // In case of a union property there might be same declaration multiple times // which only varies in type parameter // Eg. const a: Array | Array; a.length // The property length will have two declarations of property length coming // from Array - Array and Array const documentationComment = []; forEachUnique(declarations, declaration => { const comments = getJSDocComments(declaration); if (!comments) { return; } for (const comment of comments) { if (comment) { if (documentationComment.length) { documentationComment.push(lineBreakPart()); } documentationComment.push(textPart(comment)); } } }); return documentationComment; } /** * Iterates through 'array' by index and performs the callback on each element of array until the callback * returns a truthy value, then returns that value. * If no such value is found, the callback is applied to each element of array and undefined is returned. */ function forEachUnique(array: T[], callback: (element: T, index: number) => U): U { if (array) { for (let i = 0, len = array.length; i < len; i++) { if (indexOf(array, array[i]) === i) { const result = callback(array[i], i); if (result) { return result; } } } } return undefined; } export function getAllJsDocCompletionEntries(): CompletionEntry[] { return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => { return { name: tagName, kind: ScriptElementKind.keyword, kindModifiers: "", sortText: "0", }; })); } /** * Checks if position points to a valid position to add JSDoc comments, and if so, * returns the appropriate template. Otherwise returns an empty string. * Valid positions are * - outside of comments, statements, and expressions, and * - preceding a: * - function/constructor/method declaration * - class declarations * - variable statements * - namespace declarations * * Hosts should ideally check that: * - The line is all whitespace up to 'position' before performing the insertion. * - If the keystroke sequence "/\*\*" induced the call, we also check that the next * non-whitespace character is '*', which (approximately) indicates whether we added * the second '*' to complete an existing (JSDoc) comment. * @param fileName The file in which to perform the check. * @param position The (character-indexed) position in the file where the check should * be performed. */ export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number): TextInsertion { // Check if in a context where we don't want to perform any insertion if (isInString(sourceFile, position) || isInComment(sourceFile, position) || hasDocComment(sourceFile, position)) { return undefined; } const tokenAtPos = getTokenAtPosition(sourceFile, position); const tokenStart = tokenAtPos.getStart(); if (!tokenAtPos || tokenStart < position) { return undefined; } // TODO: add support for: // - enums/enum members // - interfaces // - property declarations // - potentially property assignments let commentOwner: Node; findOwner: for (commentOwner = tokenAtPos; commentOwner; commentOwner = commentOwner.parent) { switch (commentOwner.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.MethodDeclaration: case SyntaxKind.Constructor: case SyntaxKind.ClassDeclaration: case SyntaxKind.VariableStatement: break findOwner; case SyntaxKind.SourceFile: return undefined; case SyntaxKind.ModuleDeclaration: // If in walking up the tree, we hit a a nested namespace declaration, // then we must be somewhere within a dotted namespace name; however we don't // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'. if (commentOwner.parent.kind === SyntaxKind.ModuleDeclaration) { return undefined; } break findOwner; } } if (!commentOwner || commentOwner.getStart() < position) { return undefined; } const parameters = getParametersForJsDocOwningNode(commentOwner); const posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position); const lineStart = sourceFile.getLineStarts()[posLineAndChar.line]; const indentationStr = sourceFile.text.substr(lineStart, posLineAndChar.character); let docParams = ""; for (let i = 0, numParams = parameters.length; i < numParams; i++) { const currentName = parameters[i].name; const paramName = currentName.kind === SyntaxKind.Identifier ? (currentName).text : "param" + i; docParams += `${indentationStr} * @param ${paramName}${newLine}`; } // A doc comment consists of the following // * The opening comment line // * the first line (without a param) for the object's untagged info (this is also where the caret ends up) // * the '@param'-tagged lines // * TODO: other tags. // * the closing comment line // * if the caret was directly in front of the object, then we add an extra line and indentation. const preamble = "/**" + newLine + indentationStr + " * "; const result = preamble + newLine + docParams + indentationStr + " */" + (tokenStart === position ? newLine + indentationStr : ""); return { newText: result, caretOffset: preamble.length }; } function getParametersForJsDocOwningNode(commentOwner: Node): ParameterDeclaration[] { if (isFunctionLike(commentOwner)) { return commentOwner.parameters; } if (commentOwner.kind === SyntaxKind.VariableStatement) { const varStatement = commentOwner; const varDeclarations = varStatement.declarationList.declarations; if (varDeclarations.length === 1 && varDeclarations[0].initializer) { return getParametersFromRightHandSideOfAssignment(varDeclarations[0].initializer); } } return emptyArray; } /** * Digs into an an initializer or RHS operand of an assignment operation * to get the parameters of an apt signature corresponding to a * function expression or a class expression. * * @param rightHandSide the expression which may contain an appropriate set of parameters * @returns the parameters of a signature found on the RHS if one exists; otherwise 'emptyArray'. */ function getParametersFromRightHandSideOfAssignment(rightHandSide: Expression): ParameterDeclaration[] { while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) { rightHandSide = (rightHandSide).expression; } switch (rightHandSide.kind) { case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: return (rightHandSide).parameters; case SyntaxKind.ClassExpression: for (const member of (rightHandSide).members) { if (member.kind === SyntaxKind.Constructor) { return (member).parameters; } } break; } return emptyArray; } }