From 65d1079f8f10ae2b070fe5a54b942f8afc045ee5 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Thu, 1 Sep 2016 09:15:59 -0700 Subject: [PATCH] Replace services' jsdoc parser with parsers' Also modify scanner's definition of leading vs trailing comments, with associated changes to emitter and emit baselines (the last is in a separate commit). --- src/compiler/binder.ts | 6 +- src/compiler/checker.ts | 3 +- src/compiler/emitter.ts | 82 ++- src/compiler/parser.ts | 411 ++++++++++----- src/compiler/scanner.ts | 67 ++- src/compiler/types.ts | 12 +- src/compiler/utilities.ts | 162 ++++-- src/harness/unittests/jsDocParsing.ts | 728 +++++++++++++++++--------- src/services/services.ts | 404 ++------------ src/services/utilities.ts | 10 +- 10 files changed, 1053 insertions(+), 832 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index d8017d601ad..d28df13655f 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -262,7 +262,7 @@ namespace ts { Debug.assert(node.parent.kind === SyntaxKind.JSDocFunctionType); let functionType = node.parent; let index = indexOf(functionType.parameters, node); - return "p" + index; + return "arg" + index; case SyntaxKind.JSDocTypedefTag: const parentNode = node.parent && node.parent.parent; let nameFromParentNode: string; @@ -517,9 +517,7 @@ namespace ts { // because the scope of JsDocComment should not be affected by whether the current node is a // container or not. if (isInJavaScriptFile(node) && node.jsDocComments) { - for (const jsDocComment of node.jsDocComments) { - bind(jsDocComment); - } + forEach(node.jsDocComments, bind); } if (checkUnreachable(node)) { forEachChild(node, bind); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 863c9e7c37a..b6209043262 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5629,12 +5629,13 @@ namespace ts { case SyntaxKind.JSDocThisType: case SyntaxKind.JSDocOptionalType: return getTypeFromTypeNode((node).type); + case SyntaxKind.JSDocRecordType: + return getTypeFromTypeNode((node as JSDocRecordType).literal); case SyntaxKind.FunctionType: case SyntaxKind.ConstructorType: case SyntaxKind.TypeLiteral: case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.JSDocFunctionType: - case SyntaxKind.JSDocRecordType: return getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node, aliasSymbol, aliasTypeArguments); // This function assumes that an identifier or qualified name is a type expression // Callers should first ensure this by calling isTypeNode diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index f02cf266744..ce8d8e44d39 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -851,7 +851,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } } - function emitLinePreservingList(parent: Node, nodes: NodeArray, allowTrailingComma: boolean, spacesBetweenBraces: boolean) { + function emitLinePreservingList(parent: Node, nodes: NodeArray, allowTrailingComma: boolean, spacesBetweenBraces: boolean, commentsBeforePunctuation: boolean) { Debug.assert(nodes.length > 0); increaseIndent(); @@ -877,6 +877,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } emit(nodes[i]); + if (commentsBeforePunctuation) { + emitLeadingCommentsAtEnd(nodes[i]); + } } if (nodes.hasTrailingComma && allowTrailingComma) { @@ -895,7 +898,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } } - function emitList(nodes: TNode[], start: number, count: number, multiLine: boolean, trailingComma: boolean, leadingComma?: boolean, noTrailingNewLine?: boolean, emitNode?: (node: TNode) => void): number { + function emitList(nodes: TNode[], start: number, count: number, multiLine: boolean, trailingComma: boolean, leadingComma?: boolean, noTrailingNewLine?: boolean, commentsBeforePunctuation?: boolean, emitNode?: (node: TNode) => void): number { if (!emitNode) { emitNode = emit; } @@ -913,13 +916,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } } const node = nodes[start + i]; - // This emitting is to make sure we emit following comment properly - // ...(x, /*comment1*/ y)... - // ^ => node.pos - // "comment1" is not considered leading comment for "y" but rather - // considered as trailing comment of the previous node. - emitTrailingCommentsOfPosition(node.pos); emitNode(node); + if (commentsBeforePunctuation) { + emitLeadingCommentsAtEnd(nodes[i]); + } leadingComma = true; } if (trailingComma) { @@ -1897,7 +1897,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } else if (languageVersion >= ScriptTarget.ES6 || !forEach(elements, isSpreadElementExpression)) { write("["); - emitLinePreservingList(node, node.elements, elements.hasTrailingComma, /*spacesBetweenBraces*/ false); + emitLinePreservingList(node, node.elements, elements.hasTrailingComma, /*spacesBetweenBraces*/ false, /*commentsBeforePunctuation*/ true); write("]"); } else { @@ -1921,7 +1921,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge // then try to preserve the original shape of the object literal. // Otherwise just try to preserve the formatting. if (numElements === properties.length) { - emitLinePreservingList(node, properties, /*allowTrailingComma*/ languageVersion >= ScriptTarget.ES5, /*spacesBetweenBraces*/ true); + emitLinePreservingList(node, properties, /*allowTrailingComma*/ languageVersion >= ScriptTarget.ES5, /*spacesBetweenBraces*/ true, /*commentsBeforePunctuation*/ true); } else { const multiLine = node.multiLine; @@ -2166,14 +2166,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge function emitPropertyAssignment(node: PropertyDeclaration) { emit(node.name); write(": "); - // This is to ensure that we emit comment in the following case: - // For example: - // obj = { - // id: /*comment1*/ ()=>void - // } - // "comment1" is not considered to be leading comment for node.initializer - // but rather a trailing comment on the previous node. - emitTrailingCommentsOfPosition(node.initializer.pos); emit(node.initializer); } @@ -2285,6 +2277,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } emit(node.expression); + emitLeadingCommentsAtEnd(node.expression); const dotRangeStart = nodeIsSynthesized(node.expression) ? -1 : node.expression.end; const dotRangeEnd = nodeIsSynthesized(node.expression) ? -1 : skipTrivia(currentText, node.expression.end) + 1; const dotToken = { pos: dotRangeStart, end: dotRangeEnd }; @@ -2501,13 +2494,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge emitThis(expression); if (node.arguments.length) { write(", "); + emitTrailingCommentsOfPosition(node.arguments.pos); emitCommaList(node.arguments); + emitLeadingCommentsAtEnd(node.arguments); } write(")"); } else { write("("); + emitTrailingCommentsOfPosition(node.arguments.pos); emitCommaList(node.arguments); + emitLeadingCommentsAtEnd(node.arguments); write(")"); } } @@ -2608,6 +2605,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge write("("); emit(node.expression); + emitLeadingCommentsAtEnd(node.expression); write(")"); } @@ -2886,6 +2884,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } else { emit(node.left); + emitLeadingCommentsAtEnd(node.left); // Add indentation before emit the operator if the operator is on different line // For example: // 3 @@ -3898,6 +3897,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } emitNodeWithCommentsAndWithoutSourcemap(node.name); emitEnd(node.name); + emitLeadingCommentsAtEnd(node.name); } function createVoidZero(): Expression { @@ -4040,6 +4040,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge emit(name); } + emitLeadingCommentsAtEnd(name); write(" = "); emit(value); }); @@ -4591,6 +4592,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge emitStart(restParam); write("var "); emitNodeWithCommentsAndWithoutSourcemap(restParam.name); + emitLeadingCommentsAtEnd(restParam.name); write(" = [];"); emitEnd(restParam); emitTrailingComments(restParam); @@ -4612,6 +4614,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge writeLine(); emitStart(restParam); emitNodeWithCommentsAndWithoutSourcemap(restParam.name); + emitLeadingCommentsAtEnd(restParam.name); write("[" + tempName + " - " + restIndex + "] = arguments[" + tempName + "];"); emitEnd(restParam); decreaseIndent(); @@ -4633,6 +4636,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge function emitDeclarationName(node: Declaration) { if (node.name) { emitNodeWithCommentsAndWithoutSourcemap(node.name); + emitLeadingCommentsAtEnd(node.name); } else { write(getGeneratedNameForNode(node)); @@ -4660,6 +4664,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge const { kind, parent } = node; if (kind !== SyntaxKind.MethodDeclaration && kind !== SyntaxKind.MethodSignature && + kind !== SyntaxKind.ArrowFunction && parent && parent.kind !== SyntaxKind.PropertyAssignment && parent.kind !== SyntaxKind.CallExpression && @@ -4734,7 +4739,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge const parameters = node.parameters; const skipCount = node.parameters.length && (node.parameters[0].name).originalKeywordKind === SyntaxKind.ThisKeyword ? 1 : 0; const omitCount = languageVersion < ScriptTarget.ES6 && hasDeclaredRestParameter(node) ? 1 : 0; - emitList(parameters, skipCount, parameters.length - omitCount - skipCount, /*multiLine*/ false, /*trailingComma*/ false); + emitTrailingCommentsOfPosition(node.parameters.pos); + emitList(parameters, skipCount, parameters.length - omitCount - skipCount, /*multiLine*/ false, /*trailingComma*/ false, /*leadingComma*/ false, /*noTrailingNewLine*/ false, /*commentsBeforePunctuation*/ true); + if ((parameters.length - omitCount - skipCount) <= 0) { + emitLeadingCommentsAtEnd(node.parameters); + } } write(")"); decreaseIndent(); @@ -5791,7 +5800,7 @@ const _super = (function (geti, seti) { writeLine(); const decoratorCount = decorators ? decorators.length : 0; - let argumentsWritten = emitList(decorators, 0, decoratorCount, /*multiLine*/ true, /*trailingComma*/ false, /*leadingComma*/ false, /*noTrailingNewLine*/ true, + let argumentsWritten = emitList(decorators, 0, decoratorCount, /*multiLine*/ true, /*trailingComma*/ false, /*leadingComma*/ false, /*noTrailingNewLine*/ true, /*commentsBeforePunctuation*/ false, decorator => emit(decorator.expression)); if (firstParameterDecorator) { argumentsWritten += emitDecoratorsOfParameters(constructor, /*leadingComma*/ argumentsWritten > 0); @@ -5891,7 +5900,7 @@ const _super = (function (geti, seti) { writeLine(); const decoratorCount = decorators ? decorators.length : 0; - let argumentsWritten = emitList(decorators, 0, decoratorCount, /*multiLine*/ true, /*trailingComma*/ false, /*leadingComma*/ false, /*noTrailingNewLine*/ true, + let argumentsWritten = emitList(decorators, 0, decoratorCount, /*multiLine*/ true, /*trailingComma*/ false, /*leadingComma*/ false, /*noTrailingNewLine*/ true, /*commentsBeforePunctuation*/ false, decorator => emit(decorator.expression)); if (firstParameterDecorator) { @@ -5933,7 +5942,7 @@ const _super = (function (geti, seti) { for (const parameter of node.parameters) { if (nodeIsDecorated(parameter)) { const decorators = parameter.decorators; - argumentsWritten += emitList(decorators, 0, decorators.length, /*multiLine*/ true, /*trailingComma*/ false, /*leadingComma*/ leadingComma, /*noTrailingNewLine*/ true, decorator => { + argumentsWritten += emitList(decorators, 0, decorators.length, /*multiLine*/ true, /*trailingComma*/ false, /*leadingComma*/ leadingComma, /*noTrailingNewLine*/ true, /*commentsBeforePunctuation*/ false, decorator => { write(`__param(${parameterIndex}, `); emit(decorator.expression); write(")"); @@ -6347,6 +6356,7 @@ const _super = (function (geti, seti) { emitExpressionForPropertyName(node.name); emitEnd(node); write(";"); + emitLeadingCommentsAtEnd(node.name); } function writeEnumMemberDeclarationValue(member: EnumMember) { @@ -8288,7 +8298,11 @@ const _super = (function (geti, seti) { emitNewLineBeforeLeadingComments(currentLineMap, writer, node, leadingComments); // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space - emitComments(currentText, currentLineMap, writer, leadingComments, /*trailingSeparator*/ true, newLine, writeComment); + // However, a leading mid-line comment of the end of file token is emitted with spaces before, + // as if it were a trailing comment of the previous token + const commentStartsInMidLine = leadingComments && leadingComments.length && leadingComments[0].pos - 1 > 0 && currentText.charCodeAt(leadingComments[0].pos - 1) !== 10; + const isEndOfFile = node.kind === SyntaxKind.EndOfFileToken && commentStartsInMidLine; + emitComments(currentText, currentLineMap, writer, leadingComments, /*trailingSeparator*/ !isEndOfFile, newLine, writeComment); } function emitTrailingComments(node: Node) { @@ -8305,8 +8319,11 @@ const _super = (function (geti, seti) { /** * Emit trailing comments at the position. The term trailing comment is used here to describe following comment: - * x, /comment1/ y - * ^ => pos; the function will emit "comment1" in the emitJS + * x, /*comment1* / // comment2 + * ^ => pos; the function will emit "comment1" and "comment2" in the emitJS + * However, + * x /*comment1* /, y + * 'comment1' is actually a leading comment of ',' and needs to be emitted using emitLeadingCommentsAtEnd */ function emitTrailingCommentsOfPosition(pos: number) { if (compilerOptions.removeComments) { @@ -8319,7 +8336,18 @@ const _super = (function (geti, seti) { emitComments(currentText, currentLineMap, writer, trailingComments, /*trailingSeparator*/ true, newLine, writeComment); } - function emitLeadingCommentsOfPositionWorker(pos: number) { + /** + * Emit leading comments for the token after the current token. + * This is only needed for punctuation tokens. For example, in + * x /*comment1* /, y + * 'comment1' is a leading comment of ',' but needs to be emitted + * from x because there is no token for ','. + */ + function emitLeadingCommentsAtEnd(node: Node | NodeArray) { + emitLeadingCommentsOfPositionWorker(node.end, /*trailingSeparator*/ false); + } + + function emitLeadingCommentsOfPositionWorker(pos: number, trailingSeparator = true) { if (compilerOptions.removeComments) { return; } @@ -8337,7 +8365,7 @@ const _super = (function (geti, seti) { emitNewLineBeforeLeadingComments(currentLineMap, writer, { pos: pos, end: pos }, leadingComments); // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space - emitComments(currentText, currentLineMap, writer, leadingComments, /*trailingSeparator*/ true, newLine, writeComment); + emitComments(currentText, currentLineMap, writer, leadingComments, trailingSeparator, newLine, writeComment); } function emitDetachedCommentsAndUpdateCommentsInfo(node: TextRange) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index b28f13e438f..342b711a5a6 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -378,7 +378,7 @@ namespace ts { case SyntaxKind.JSDocNullableType: return visitNode(cbNode, (node).type); case SyntaxKind.JSDocRecordType: - return visitNodes(cbNodes, (node).members); + return visitNode(cbNode, (node).literal); case SyntaxKind.JSDocTypeReference: return visitNode(cbNode, (node).name) || visitNodes(cbNodes, (node).typeArguments); @@ -397,7 +397,7 @@ namespace ts { return visitNode(cbNode, (node).name) || visitNode(cbNode, (node).type); case SyntaxKind.JSDocComment: - return visitNodes(cbNodes, (node).tags); + return visitNodes(cbNodes, (node).tags); case SyntaxKind.JSDocParameterTag: return visitNode(cbNode, (node).preParameterName) || visitNode(cbNode, (node).typeExpression) || @@ -450,10 +450,10 @@ namespace ts { /* @internal */ export function parseIsolatedJSDocComment(content: string, start?: number, length?: number) { const result = Parser.JSDocParser.parseIsolatedJSDocComment(content, start, length); - if (result && result.jsDocComment) { + if (result && result.jsDoc) { // because the jsDocComment was parsed out of the source file, it might // not be covered by the fixupParentReferences. - Parser.fixupParentReferences(result.jsDocComment); + Parser.fixupParentReferences(result.jsDoc); } return result; @@ -652,20 +652,18 @@ namespace ts { function addJSDocComment(node: T): T { - if (contextFlags & NodeFlags.JavaScriptFile) { - const comments = getLeadingCommentRangesOfNode(node, sourceFile); - if (comments) { - for (const comment of comments) { - const jsDocComment = JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos); - if (!jsDocComment) { - continue; - } - - if (!node.jsDocComments) { - node.jsDocComments = []; - } - node.jsDocComments.push(jsDocComment); + const comments = getLeadingCommentRangesOfNode(node, sourceFile); + if (comments) { + for (const comment of comments) { + const jsDoc = JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos); + if (!jsDoc) { + continue; } + + if (!node.jsDocComments) { + node.jsDocComments = []; + } + node.jsDocComments.push(jsDoc); } } @@ -2201,7 +2199,7 @@ namespace ts { } fillSignature(SyntaxKind.ColonToken, /*yieldContext*/ false, /*awaitContext*/ false, /*requireCompleteParameterList*/ false, node); parseTypeMemberSemicolon(); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function isIndexSignature(): boolean { @@ -2291,7 +2289,7 @@ namespace ts { // [Yield] nor [Await] fillSignature(SyntaxKind.ColonToken, /*yieldContext*/ false, /*awaitContext*/ false, /*requireCompleteParameterList*/ false, method); parseTypeMemberSemicolon(); - return finishNode(method); + return addJSDocComment(finishNode(method)); } else { const property = createNode(SyntaxKind.PropertySignature, fullStart); @@ -2308,7 +2306,7 @@ namespace ts { } parseTypeMemberSemicolon(); - return finishNode(property); + return addJSDocComment(finishNode(property)); } } @@ -2897,7 +2895,7 @@ namespace ts { node.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, "=>"); node.body = parseArrowFunctionExpressionBody(/*isAsync*/ !!asyncModifier); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function tryParseParenthesizedArrowFunctionExpression(): Expression { @@ -2930,7 +2928,7 @@ namespace ts { ? parseArrowFunctionExpressionBody(isAsync) : parseIdentifier(); - return finishNode(arrowFunction); + return addJSDocComment(finishNode(arrowFunction)); } // True -> We definitely expect a parenthesized arrow function here. @@ -4114,7 +4112,7 @@ namespace ts { function tryParseAccessorDeclaration(fullStart: number, decorators: NodeArray, modifiers: ModifiersArray): AccessorDeclaration { if (parseContextualModifier(SyntaxKind.GetKeyword)) { - return addJSDocComment(parseAccessorDeclaration(SyntaxKind.GetAccessor, fullStart, decorators, modifiers)); + return parseAccessorDeclaration(SyntaxKind.GetAccessor, fullStart, decorators, modifiers); } else if (parseContextualModifier(SyntaxKind.SetKeyword)) { return parseAccessorDeclaration(SyntaxKind.SetAccessor, fullStart, decorators, modifiers); @@ -4993,7 +4991,7 @@ namespace ts { : doOutsideOfContext(NodeFlags.YieldContext | NodeFlags.DisallowInContext, parseNonParameterInitializer); parseSemicolon(); - return finishNode(property); + return addJSDocComment(finishNode(property)); } function parsePropertyOrMethodDeclaration(fullStart: number, decorators: NodeArray, modifiers: ModifiersArray): ClassElement { @@ -5022,7 +5020,7 @@ namespace ts { node.name = parsePropertyName(); fillSignature(SyntaxKind.ColonToken, /*yieldContext*/ false, /*awaitContext*/ false, /*requireCompleteParameterList*/ false, node); node.body = parseFunctionBlockOrSemicolon(/*isGenerator*/ false, /*isAsync*/ false); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function isClassMemberModifier(idToken: SyntaxKind) { @@ -5265,7 +5263,7 @@ namespace ts { node.members = createMissingList(); } - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseNameOfClassDeclarationOrExpression(): Identifier { @@ -5333,7 +5331,7 @@ namespace ts { node.typeParameters = parseTypeParameters(); node.heritageClauses = parseHeritageClauses(/*isClassHeritageClause*/ false); node.members = parseObjectTypeMembers(); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseTypeAliasDeclaration(fullStart: number, decorators: NodeArray, modifiers: ModifiersArray): TypeAliasDeclaration { @@ -5357,7 +5355,7 @@ namespace ts { const node = createNode(SyntaxKind.EnumMember, scanner.getStartPos()); node.name = parsePropertyName(); node.initializer = allowInAnd(parseNonParameterInitializer); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseEnumDeclaration(fullStart: number, decorators: NodeArray, modifiers: ModifiersArray): EnumDeclaration { @@ -5373,7 +5371,7 @@ namespace ts { else { node.members = createMissingList(); } - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseModuleBlock(): ModuleBlock { @@ -5400,7 +5398,7 @@ namespace ts { node.body = parseOptional(SyntaxKind.DotToken) ? parseModuleOrNamespaceDeclaration(getNodePos(), /*decorators*/ undefined, /*modifiers*/ undefined, NodeFlags.Export | namespaceFlag) : parseModuleBlock(); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseAmbientExternalModuleDeclaration(fullStart: number, decorators: NodeArray, modifiers: ModifiersArray): ModuleDeclaration { @@ -5489,7 +5487,7 @@ namespace ts { parseExpected(SyntaxKind.EqualsToken); importEqualsDeclaration.moduleReference = parseModuleReference(); parseSemicolon(); - return finishNode(importEqualsDeclaration); + return addJSDocComment(finishNode(importEqualsDeclaration)); } } @@ -5810,6 +5808,7 @@ namespace ts { export function parseJSDocTypeExpressionForTests(content: string, start: number, length: number) { initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); + sourceFile = createSourceFile("file.js", ScriptTarget.Latest, ScriptKind.JS); scanner.setText(content, start, length); currentToken = scanner.scan(); const jsDocTypeExpression = parseJSDocTypeExpression(); @@ -6028,22 +6027,7 @@ namespace ts { function parseJSDocRecordType(): JSDocRecordType { const result = createNode(SyntaxKind.JSDocRecordType); - nextToken(); - result.members = parseDelimitedList(ParsingContext.JSDocRecordMembers, parseJSDocRecordMember); - checkForTrailingComma(result.members); - parseExpected(SyntaxKind.CloseBraceToken); - return finishNode(result); - } - - function parseJSDocRecordMember(): JSDocRecordMember { - const result = createNode(SyntaxKind.JSDocRecordMember); - result.name = parseSimplePropertyName(); - - if (token() === SyntaxKind.ColonToken) { - nextToken(); - result.type = parseJSDocType(); - } - + result.literal = parseTypeLiteral(); return finishNode(result); } @@ -6143,14 +6127,14 @@ namespace ts { export function parseIsolatedJSDocComment(content: string, start: number, length: number) { initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); sourceFile = { languageVariant: LanguageVariant.Standard, text: content }; - const jsDocComment = parseJSDocCommentWorker(start, length); + const jsDoc = parseJSDocCommentWorker(start, length); const diagnostics = parseDiagnostics; clearState(); - return jsDocComment ? { jsDocComment, diagnostics } : undefined; + return jsDoc ? { jsDoc, diagnostics } : undefined; } - export function parseJSDocComment(parent: Node, start: number, length: number): JSDocComment { + export function parseJSDocComment(parent: Node, start: number, length: number): JSDoc { const saveToken = currentToken; const saveParseDiagnosticsLength = parseDiagnostics.length; const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; @@ -6167,7 +6151,7 @@ namespace ts { return comment; } - export function parseJSDocCommentWorker(start: number, length: number): JSDocComment { + export function parseJSDocCommentWorker(start: number, length: number): JSDoc { const content = sourceText; start = start || 0; const end = length === undefined ? content.length : start + length; @@ -6178,76 +6162,145 @@ namespace ts { Debug.assert(end <= content.length); let tags: NodeArray; - let result: JSDocComment; + const comments: string[] = []; + let result: JSDoc; // Check for /** (JSDoc opening part) - if (content.charCodeAt(start) === CharacterCodes.slash && - content.charCodeAt(start + 1) === CharacterCodes.asterisk && - content.charCodeAt(start + 2) === CharacterCodes.asterisk && - content.charCodeAt(start + 3) !== CharacterCodes.asterisk) { + if (!isJsDocStart(content, start)) { + return result; + } + // + 3 for leading /**, - 5 in total for /** */ + scanner.scanRange(start + 3, length - 5, () => { + // Initially we can parse out a tag. We also have seen a starting asterisk. + // This is so that /** * @type */ doesn't parse. + let canParseTag = true; + let seenAsterisk = true; + let advanceToken = true; + let margin: number | undefined = undefined; + let indent = start - Math.max(content.lastIndexOf("\n", start), 0) + 4; + let text: string; - // + 3 for leading /**, - 5 in total for /** */ - scanner.scanRange(start + 3, length - 5, () => { - // Initially we can parse out a tag. We also have seen a starting asterisk. - // This is so that /** * @type */ doesn't parse. - let canParseTag = true; - let seenAsterisk = true; - + nextJSDocToken(); + while (token() === SyntaxKind.WhitespaceTrivia) { nextJSDocToken(); - while (token() !== SyntaxKind.EndOfFileToken) { - switch (token()) { - case SyntaxKind.AtToken: - if (canParseTag) { - parseTag(); - } - // This will take us to the end of the line, so it's OK to parse a tag on the next pass through the loop + } + if (token() === SyntaxKind.NewLineTrivia) { + canParseTag = true; + seenAsterisk = false; + nextJSDocToken(); + } + while (token() !== SyntaxKind.EndOfFileToken) { + switch (token()) { + case SyntaxKind.AtToken: + if (canParseTag) { + removeTrailingNewlines(comments); + parseTag(indent); + // This will take us past the end of the line, so it's OK to parse a tag on the next pass through the loop + // NOTE: According to usejsdoc.org, a tag goes to end of line, except the last tag. But real-world comments may break this rule. seenAsterisk = false; - break; + advanceToken = false; + margin = undefined; + } + else { + comments.push(scanner.getTokenText()); + } + indent++; + break; - case SyntaxKind.NewLineTrivia: - // After a line break, we can parse a tag, and we haven't seen an asterisk on the next line yet - canParseTag = true; - seenAsterisk = false; - break; + case SyntaxKind.NewLineTrivia: + // After a line break, we can parse a tag, and we haven't seen an asterisk on the next line yet + comments.push(scanner.getTokenText()); + canParseTag = true; + seenAsterisk = false; + indent = 0; + break; - case SyntaxKind.AsteriskToken: - if (seenAsterisk) { - // If we've already seen an asterisk, then we can no longer parse a tag on this line - canParseTag = false; - } - // Ignore the first asterisk on a line - seenAsterisk = true; - break; - - case SyntaxKind.Identifier: - // Anything else is doc comment text. We can't do anything with it. Because it - // wasn't a tag, we can no longer parse a tag on this line until we hit the next - // line break. + case SyntaxKind.AsteriskToken: + text = scanner.getTokenText(); + if (seenAsterisk) { + // If we've already seen an asterisk, then we can no longer parse a tag on this line canParseTag = false; - break; + comments.push(text); + if (!margin) { + margin = indent; + } + } + // Ignore the first asterisk on a line + seenAsterisk = true; + indent += text.length; + break; - case SyntaxKind.EndOfFileToken: - break; - } + case SyntaxKind.Identifier: + // Anything else is doc comment text. We just save it. Because it + // wasn't a tag, we can no longer parse a tag on this line until we hit the next + // line break. + text = scanner.getTokenText(); + comments.push(text); + if (!margin) { + margin = indent; + } + canParseTag = false; + indent += text.length; + break; + + case SyntaxKind.WhitespaceTrivia: + // only collect whitespace if we *know* that we're just looking at comments, not a possible jsdoc tag + text = scanner.getTokenText(); + if (!canParseTag || margin !== undefined && indent + text.length > margin) { + comments.push(text.slice(margin - indent - 1)); + } + indent += text.length; + break; + + case SyntaxKind.EndOfFileToken: + break; + + default: + text = scanner.getTokenText(); + comments.push(text); + indent += text.length; + break; + } + if (advanceToken) { nextJSDocToken(); } + else { + advanceToken = true; + } + } + removeLeadingNewlines(comments); + removeTrailingNewlines(comments); + result = createJSDocComment(); - result = createJSDocComment(); - - }); - } + }); return result; - function createJSDocComment(): JSDocComment { - if (!tags) { - return undefined; + function removeLeadingNewlines(comments: string[]) { + while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { + comments.shift(); } + } - const result = createNode(SyntaxKind.JSDocComment, start); + function removeTrailingNewlines(comments: string[]) { + while (comments.length && (comments[comments.length - 1] === "\n" || comments[comments.length - 1] === "\r")) { + comments.pop(); + } + } + + function isJsDocStart(content: string, start: number) { + return content.charCodeAt(start) === CharacterCodes.slash && + content.charCodeAt(start + 1) === CharacterCodes.asterisk && + content.charCodeAt(start + 2) === CharacterCodes.asterisk && + content.charCodeAt(start + 3) !== CharacterCodes.asterisk; + } + + function createJSDocComment(): JSDoc { + const result = createNode(SyntaxKind.JSDocComment, start); result.tags = tags; + result.comment = comments.length ? comments.join("") : undefined; return finishNode(result, end); } @@ -6257,78 +6310,158 @@ namespace ts { } } - function parseTag(): void { + function parseTag(indent: number) { Debug.assert(token() === SyntaxKind.AtToken); const atToken = createNode(SyntaxKind.AtToken, scanner.getTokenPos()); atToken.end = scanner.getTextPos(); nextJSDocToken(); const tagName = parseJSDocIdentifierName(); + skipWhitespace(); if (!tagName) { return; } - const tag = handleTag(atToken, tagName) || handleUnknownTag(atToken, tagName); - addTag(tag); - } - - function handleTag(atToken: Node, tagName: Identifier): JSDocTag { + let tag: JSDocTag; if (tagName) { switch (tagName.text) { case "param": - return handleParamTag(atToken, tagName); + tag = parseParamTag(atToken, tagName); + break; case "return": case "returns": - return handleReturnTag(atToken, tagName); + tag = parseReturnTag(atToken, tagName); + break; case "template": - return handleTemplateTag(atToken, tagName); + tag = parseTemplateTag(atToken, tagName); + break; case "type": - return handleTypeTag(atToken, tagName); + tag = parseTypeTag(atToken, tagName); + break; case "typedef": - return handleTypedefTag(atToken, tagName); + tag = parseTypedefTag(atToken, tagName); + break; + default: + tag = parseUnknownTag(atToken, tagName); + break; + } + } + else { + tag = parseUnknownTag(atToken, tagName); + } + + if (!tag) { + // a badly malformed tag should not be added to the list of tags + return; + } + addTag(tag, parseTagComments(indent + tag.end - tag.pos)); + } + + function parseTagComments(indent: number) { + const comments: string[] = []; + let savingComments = false; + let seenAsterisk = true; + let done = false; + let margin: number | undefined; + let text: string; + function pushComment(text: string) { + if (!margin) { + margin = indent; + } + comments.push(text); + indent += text.length; + } + while (!done && token() !== SyntaxKind.EndOfFileToken) { + text = scanner.getTokenText(); + switch (token()) { + case SyntaxKind.NewLineTrivia: + if (seenAsterisk) { + savingComments = false; + seenAsterisk = false; + comments.push(text); + } + indent = 0; + break; + case SyntaxKind.AtToken: + done = true; + break; + case SyntaxKind.WhitespaceTrivia: + if (savingComments && seenAsterisk) { + pushComment(text); + } + else { + // if the whitespace crosses the margin, take only the whitespace that passes the margin + if (margin !== undefined && indent + text.length > margin) { + comments.push(text.slice(margin - indent - 1)); + } + indent += text.length; + } + break; + case SyntaxKind.AsteriskToken: + if (!seenAsterisk) { + // leading asterisks start recording on the *next* (non-whitespace) token + savingComments = false; + indent += text.length; + } + // FALLTHROUGH to gather comments + default: + if (seenAsterisk) { + savingComments = true; // leading identifiers start recording as well + pushComment(text); + } + seenAsterisk = true; + break; + } + if (!done) { + nextJSDocToken(); } } - return undefined; + removeLeadingNewlines(comments); + removeTrailingNewlines(comments); + return comments; } - function handleUnknownTag(atToken: Node, tagName: Identifier) { + function parseUnknownTag(atToken: Node, tagName: Identifier) { const result = createNode(SyntaxKind.JSDocTag, atToken.pos); result.atToken = atToken; result.tagName = tagName; return finishNode(result); } - function addTag(tag: JSDocTag): void { - if (tag) { - if (!tags) { - tags = >[]; - tags.pos = tag.pos; - } + function addTag(tag: JSDocTag, comments: string[]): void { + tag.comment = comments.join(""); - tags.push(tag); - tags.end = tag.end; + if (!tags) { + tags = >[]; + tags.pos = tag.pos; } + + tags.push(tag); + tags.end = tag.end; } function tryParseTypeExpression(): JSDocTypeExpression { - if (token() !== SyntaxKind.OpenBraceToken) { - return undefined; - } + return tryParse(() => { + skipWhitespace(); + if (token() !== SyntaxKind.OpenBraceToken) { + return undefined; + } - const typeExpression = parseJSDocTypeExpression(); - return typeExpression; + return parseJSDocTypeExpression(); + }); } - function handleParamTag(atToken: Node, tagName: Identifier) { + function parseParamTag(atToken: Node, tagName: Identifier) { let typeExpression = tryParseTypeExpression(); - skipWhitespace(); + let name: Identifier; let isBracketed: boolean; // Looking for something like '[foo]' or 'foo' if (parseOptionalToken(SyntaxKind.OpenBracketToken)) { name = parseJSDocIdentifierName(); + skipWhitespace(); isBracketed = true; // May have an optional default, e.g. '[foo = 42]' @@ -6365,11 +6498,12 @@ namespace ts { result.preParameterName = preName; result.typeExpression = typeExpression; result.postParameterName = postName; + result.parameterName = postName || preName; result.isBracketed = isBracketed; return finishNode(result); } - function handleReturnTag(atToken: Node, tagName: Identifier): JSDocReturnTag { + function parseReturnTag(atToken: Node, tagName: Identifier): JSDocReturnTag { if (forEach(tags, t => t.kind === SyntaxKind.JSDocReturnTag)) { parseErrorAtPosition(tagName.pos, scanner.getTokenPos() - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); } @@ -6381,7 +6515,7 @@ namespace ts { return finishNode(result); } - function handleTypeTag(atToken: Node, tagName: Identifier): JSDocTypeTag { + function parseTypeTag(atToken: Node, tagName: Identifier): JSDocTypeTag { if (forEach(tags, t => t.kind === SyntaxKind.JSDocTypeTag)) { parseErrorAtPosition(tagName.pos, scanner.getTokenPos() - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); } @@ -6393,10 +6527,11 @@ namespace ts { return finishNode(result); } - function handlePropertyTag(atToken: Node, tagName: Identifier): JSDocPropertyTag { + function parsePropertyTag(atToken: Node, tagName: Identifier): JSDocPropertyTag { const typeExpression = tryParseTypeExpression(); skipWhitespace(); const name = parseJSDocIdentifierName(); + skipWhitespace(); if (!name) { parseErrorAtPosition(scanner.getStartPos(), /*length*/ 0, Diagnostics.Identifier_expected); return undefined; @@ -6410,7 +6545,7 @@ namespace ts { return finishNode(result); } - function handleTypedefTag(atToken: Node, tagName: Identifier): JSDocTypedefTag { + function parseTypedefTag(atToken: Node, tagName: Identifier): JSDocTypedefTag { const typeExpression = tryParseTypeExpression(); skipWhitespace(); @@ -6419,6 +6554,7 @@ namespace ts { typedefTag.tagName = tagName; typedefTag.name = parseJSDocIdentifierName(); typedefTag.typeExpression = typeExpression; + skipWhitespace(); if (typeExpression) { if (typeExpression.type.kind === SyntaxKind.JSDocTypeReference) { @@ -6488,6 +6624,7 @@ namespace ts { nextJSDocToken(); const tagName = parseJSDocIdentifierName(); + skipWhitespace(); if (!tagName) { return false; } @@ -6498,21 +6635,21 @@ namespace ts { // already has a @type tag, terminate the parent tag now. return false; } - parentTag.jsDocTypeTag = handleTypeTag(atToken, tagName); + parentTag.jsDocTypeTag = parseTypeTag(atToken, tagName); return true; case "prop": case "property": if (!parentTag.jsDocPropertyTags) { parentTag.jsDocPropertyTags = >[]; } - const propertyTag = handlePropertyTag(atToken, tagName); + const propertyTag = parsePropertyTag(atToken, tagName); parentTag.jsDocPropertyTags.push(propertyTag); return true; } return false; } - function handleTemplateTag(atToken: Node, tagName: Identifier): JSDocTemplateTag { + function parseTemplateTag(atToken: Node, tagName: Identifier): JSDocTemplateTag { if (forEach(tags, t => t.kind === SyntaxKind.JSDocTemplateTag)) { parseErrorAtPosition(tagName.pos, scanner.getTokenPos() - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); } @@ -6523,6 +6660,7 @@ namespace ts { while (true) { const name = parseJSDocIdentifierName(); + skipWhitespace(); if (!name) { parseErrorAtPosition(scanner.getStartPos(), 0, Diagnostics.Identifier_expected); return undefined; @@ -6536,6 +6674,7 @@ namespace ts { if (token() === SyntaxKind.CommaToken) { nextJSDocToken(); + skipWhitespace(); } else { break; diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index c1431ca23ed..d8cfc39db87 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -599,10 +599,11 @@ namespace ts { * If false, whitespace is skipped until the first line break and comments between that location * and the next token are returned. * If true, comments occurring between the given position and the next line break are returned. + * If there is no line break, then no comments are returned. */ function getCommentRanges(text: string, pos: number, trailing: boolean): CommentRange[] { let result: CommentRange[]; - let collecting = trailing || pos === 0; + let needToSkipTrailingComment = pos > 0; while (pos < text.length) { const ch = text.charCodeAt(pos); switch (ch) { @@ -615,7 +616,11 @@ namespace ts { if (trailing) { return result; } - collecting = true; + else if (needToSkipTrailingComment) { + // skip the first line if not trailing (it'll be part of the trailing comment). + needToSkipTrailingComment = false; + result = undefined; + } if (result && result.length) { lastOrUndefined(result).hasTrailingNewLine = true; } @@ -651,11 +656,13 @@ namespace ts { pos++; } } - if (collecting) { + + // if we are at the end of the file, don't add trailing comments. + // end-of-file comments are added as leading comment of the end-of-file token. + if (pos < text.length || !trailing) { if (!result) { result = []; } - result.push({ pos: startPos, end: pos, hasTrailingNewLine, kind }); } continue; @@ -671,10 +678,10 @@ namespace ts { } break; } - return result; + return !trailing ? result : undefined; } - return result; + return !trailing ? result : undefined; } export function getLeadingCommentRanges(text: string, pos: number): CommentRange[] { @@ -1689,40 +1696,46 @@ namespace ts { } startPos = pos; - - // Eat leading whitespace - let ch = text.charCodeAt(pos); - while (pos < end) { - ch = text.charCodeAt(pos); - if (isWhiteSpaceSingleLine(ch)) { - pos++; - } - else { - break; - } - } tokenPos = pos; + const ch = text.charCodeAt(pos); switch (ch) { + case CharacterCodes.tab: + case CharacterCodes.verticalTab: + case CharacterCodes.formFeed: + case CharacterCodes.space: + while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { + pos++; + } + return token = SyntaxKind.WhitespaceTrivia; case CharacterCodes.at: - return pos += 1, token = SyntaxKind.AtToken; + pos++; + return token = SyntaxKind.AtToken; case CharacterCodes.lineFeed: case CharacterCodes.carriageReturn: - return pos += 1, token = SyntaxKind.NewLineTrivia; + pos++; + return token = SyntaxKind.NewLineTrivia; case CharacterCodes.asterisk: - return pos += 1, token = SyntaxKind.AsteriskToken; + pos++; + return token = SyntaxKind.AsteriskToken; case CharacterCodes.openBrace: - return pos += 1, token = SyntaxKind.OpenBraceToken; + pos++; + return token = SyntaxKind.OpenBraceToken; case CharacterCodes.closeBrace: - return pos += 1, token = SyntaxKind.CloseBraceToken; + pos++; + return token = SyntaxKind.CloseBraceToken; case CharacterCodes.openBracket: - return pos += 1, token = SyntaxKind.OpenBracketToken; + pos++; + return token = SyntaxKind.OpenBracketToken; case CharacterCodes.closeBracket: - return pos += 1, token = SyntaxKind.CloseBracketToken; + pos++; + return token = SyntaxKind.CloseBracketToken; case CharacterCodes.equals: - return pos += 1, token = SyntaxKind.EqualsToken; + pos++; + return token = SyntaxKind.EqualsToken; case CharacterCodes.comma: - return pos += 1, token = SyntaxKind.CommaToken; + pos++; + return token = SyntaxKind.CommaToken; } if (isIdentifierStart(ch, ScriptTarget.Latest)) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 50beef52bf8..2ee28d501a9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -464,7 +464,7 @@ namespace ts { modifiers?: ModifiersArray; // Array of modifiers /* @internal */ id?: number; // Unique id (used to look up NodeLinks) parent?: Node; // Parent node (initialized by binding - /* @internal */ jsDocComments?: JSDocComment[]; // JSDoc for the node, if it has any. Only for .js files. + /* @internal */ jsDocComments?: JSDoc[]; // JSDoc for the node, if it has any. Only for .js files. /* @internal */ symbol?: Symbol; // Symbol declared by node (initialized by binding) /* @internal */ locals?: SymbolTable; // Locals associated with node (initialized by binding) /* @internal */ nextContainer?: Node; // Next container in declaration order (initialized by binding) @@ -1471,7 +1471,7 @@ namespace ts { // @kind(SyntaxKind.JSDocRecordType) export interface JSDocRecordType extends JSDocType, TypeLiteralNode { - members: NodeArray; + literal: TypeLiteralNode; } // @kind(SyntaxKind.JSDocTypeReference) @@ -1519,14 +1519,16 @@ namespace ts { } // @kind(SyntaxKind.JSDocComment) - export interface JSDocComment extends Node { + export interface JSDoc extends Node { tags: NodeArray; + comment: string | undefined; } // @kind(SyntaxKind.JSDocTag) export interface JSDocTag extends Node { atToken: Node; tagName: Identifier; + comment: string | undefined; } // @kind(SyntaxKind.JSDocTemplateTag) @@ -1565,9 +1567,13 @@ namespace ts { // @kind(SyntaxKind.JSDocParameterTag) export interface JSDocParameterTag extends JSDocTag { + /** the parameter name, if provided *before* the type (TypeScript-style) */ preParameterName?: Identifier; typeExpression?: JSDocTypeExpression; + /** the parameter name, if provided *after* the type (JSDoc-standard) */ postParameterName?: Identifier; + /** the parameter name, regardless of the location it was provided */ + parameterName: Identifier; isBracketed: boolean; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 0c026fc6b6d..52683f0e74b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -596,13 +596,7 @@ namespace ts { } export function getJsDocCommentsFromText(node: Node, text: string) { - const commentRanges = (node.kind === SyntaxKind.Parameter || - node.kind === SyntaxKind.TypeParameter || - node.kind === SyntaxKind.FunctionExpression || - node.kind === SyntaxKind.ArrowFunction) ? - concatenate(getTrailingCommentRanges(text, node.pos), getLeadingCommentRanges(text, node.pos)) : - getLeadingCommentRangesOfNodeFromText(node, text); - return filter(commentRanges, isJsDocComment); + return filter(getLeadingCommentRanges(text, node.pos), isJsDocComment); function isJsDocComment(comment: CommentRange) { // True if the comment starts with '/**' but not if it is '/**/' @@ -1356,39 +1350,75 @@ namespace ts { return undefined; } - const jsDocComments = getJSDocComments(node, checkParentVariableStatement); - if (!jsDocComments) { + const jsDocTags = getJSDocTags(node, checkParentVariableStatement); + if (!jsDocTags) { return undefined; } - for (const jsDocComment of jsDocComments) { - for (const tag of jsDocComment.tags) { - if (tag.kind === kind) { - return tag; - } + for (const tag of jsDocTags) { + if (tag.kind === kind) { + return tag; } } } - function getJSDocComments(node: Node, checkParentVariableStatement: boolean): JSDocComment[] { - if (node.jsDocComments) { - return node.jsDocComments; + function append(previous: T[] | undefined, additional: T[] | undefined): T[] | undefined { + if (additional) { + if (!previous) { + previous = []; + } + for (const x of additional) { + previous.push(x); + } } - // Try to recognize this pattern when node is initializer of variable declaration and JSDoc comments are on containing variable statement. - // /** - // * @param {number} name - // * @returns {number} - // */ - // var x = function(name) { return name.length; } - if (checkParentVariableStatement) { - const isInitializerOfVariableDeclarationInStatement = - node.parent.kind === SyntaxKind.VariableDeclaration && - (node.parent).initializer === node && - node.parent.parent.parent.kind === SyntaxKind.VariableStatement; + return previous; + } - const variableStatementNode = isInitializerOfVariableDeclarationInStatement ? node.parent.parent.parent : undefined; + export function getJSDocComments(node: Node, checkParentVariableStatement: boolean): string[] { + return getJSDocs(node, checkParentVariableStatement, docs => map(docs, doc => doc.comment), tags => map(tags, tag => tag.comment)); + } + + function getJSDocTags(node: Node, checkParentVariableStatement: boolean): JSDocTag[] { + return getJSDocs(node, checkParentVariableStatement, docs => { + let result: JSDocTag[] = []; + for (const doc of docs) { + if (doc.tags) { + result.push(...doc.tags); + } + } + return result; + }, tags => tags); + } + + function getJSDocs(node: Node, checkParentVariableStatement: boolean, getDocs: (docs: JSDoc[]) => T[], getTags: (tags: JSDocTag[]) => T[]): T[] { + // TODO: Get rid of getJsDocComments and friends (note the lowercase 's' in Js) + // TODO: A lot of this work should be cached, maybe. I guess it's only used in services right now... + let result: T[] = undefined; + // prepend documentation from parent sources + if (checkParentVariableStatement) { + // Try to recognize this pattern when node is initializer of variable declaration and JSDoc comments are on containing variable statement. + // /** + // * @param {number} name + // * @returns {number} + // */ + // var x = function(name) { return name.length; } + const isInitializerOfVariableDeclarationInStatement = + isVariableLike(node.parent) && + (node.parent).initializer === node && + node.parent.parent.parent.kind === SyntaxKind.VariableStatement; + const isVariableOfVariableDeclarationStatement = isVariableLike(node) && + node.parent.parent.kind === SyntaxKind.VariableStatement; + + const variableStatementNode = + isInitializerOfVariableDeclarationInStatement ? node.parent.parent.parent : + isVariableOfVariableDeclarationStatement ? node.parent.parent : + undefined; if (variableStatementNode) { - return variableStatementNode.jsDocComments; + result = append(result, getJSDocs(variableStatementNode, checkParentVariableStatement, getDocs, getTags)); + } + if (node.kind === SyntaxKind.ModuleDeclaration && + node.parent && node.parent.kind === SyntaxKind.ModuleDeclaration) { + result = append(result, getJSDocs(node.parent, checkParentVariableStatement, getDocs, getTags)); } // Also recognize when the node is the RHS of an assignment expression @@ -1399,16 +1429,62 @@ namespace ts { (parent as BinaryExpression).operatorToken.kind === SyntaxKind.EqualsToken && parent.parent.kind === SyntaxKind.ExpressionStatement; if (isSourceOfAssignmentExpressionStatement) { - return parent.parent.jsDocComments; + result = append(result, getJSDocs(parent.parent, checkParentVariableStatement, getDocs, getTags)); } const isPropertyAssignmentExpression = parent && parent.kind === SyntaxKind.PropertyAssignment; if (isPropertyAssignmentExpression) { - return parent.jsDocComments; + result = append(result, getJSDocs(parent, checkParentVariableStatement, getDocs, getTags)); + } + + // Pull parameter comments from declaring function as well + if (node.kind === SyntaxKind.Parameter) { + const paramTags = getJSDocParameterTag(node as ParameterDeclaration, checkParentVariableStatement); + if (paramTags) { + result = append(result, getTags(paramTags)); + } } } - return undefined; + if (isVariableLike(node) && node.initializer) { + result = append(result, getJSDocs(node.initializer, /*checkParentVariableStatement*/ false, getDocs, getTags)); + } + + if (node.jsDocComments) { + if (result) { + result = append(result, getDocs(node.jsDocComments)); + } + else { + return getDocs(node.jsDocComments); + } + } + + return result; + } + + function getJSDocParameterTag(param: ParameterDeclaration, checkParentVariableStatement: boolean): JSDocTag[] { + const func = param.parent as FunctionLikeDeclaration; + const tags = getJSDocTags(func, checkParentVariableStatement); + if (!param.name) { + // this is an anonymous jsdoc param from a `function(type1, type2): type3` specification + const i = func.parameters.indexOf(param); + const paramTags = filter(tags, tag => tag.kind === SyntaxKind.JSDocParameterTag); + if (paramTags && 0 <= i && i < paramTags.length) { + return [paramTags[i]]; + } + } + else if (param.name.kind === SyntaxKind.Identifier) { + const name = (param.name as Identifier).text; + const paramTags = filter(tags, tag => tag.kind === SyntaxKind.JSDocParameterTag && (tag as JSDocParameterTag).parameterName.text === name); + if (paramTags) { + return paramTags; + } + } + else { + // TODO: it's a destructured parameter, so it should look up an "object type" series of multiple lines + // But multi-line object types aren't supported yet either + return undefined; + } } export function getJSDocTypeTag(node: Node): JSDocTypeTag { @@ -1429,17 +1505,15 @@ namespace ts { // annotation. const parameterName = (parameter.name).text; - const jsDocComments = getJSDocComments(parameter.parent, /*checkParentVariableStatement*/ true); - if (jsDocComments) { - for (const jsDocComment of jsDocComments) { - for (const tag of jsDocComment.tags) { - if (tag.kind === SyntaxKind.JSDocParameterTag) { - const parameterTag = tag; - const name = parameterTag.preParameterName || parameterTag.postParameterName; - if (name.text === parameterName) { - return parameterTag; - } - } + const jsDocTags = getJSDocTags(parameter.parent, /*checkParentVariableStatement*/ true); + if (!jsDocTags) { + return undefined; + } + for (const tag of jsDocTags) { + if (tag.kind === SyntaxKind.JSDocParameterTag) { + const parameterTag = tag; + if (parameterTag.parameterName.text === parameterName) { + return parameterTag; } } } diff --git a/src/harness/unittests/jsDocParsing.ts b/src/harness/unittests/jsDocParsing.ts index a9987ef3176..e688dc20750 100644 --- a/src/harness/unittests/jsDocParsing.ts +++ b/src/harness/unittests/jsDocParsing.ts @@ -6,11 +6,11 @@ namespace ts { describe("TypeExpressions", () => { function parsesCorrectly(content: string, expected: string) { const typeAndDiagnostics = ts.parseJSDocTypeExpressionForTests(content); - assert.isTrue(typeAndDiagnostics && typeAndDiagnostics.diagnostics.length === 0); + assert.isTrue(typeAndDiagnostics && typeAndDiagnostics.diagnostics.length === 0, "no errors issued"); const result = Utils.sourceFileToJSON(typeAndDiagnostics.jsDocTypeExpression.type); - assert.equal(result, expected); + assert.equal(result, expected, "result === expected"); } function parsesIncorrectly(content: string) { @@ -99,10 +99,15 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 3, - "members": { - "length": 0, - "pos": 2, - "end": 2 + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 3, + "members": { + "length": 0, + "pos": 2, + "end": 2 + } } }`); }); @@ -113,21 +118,26 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 6, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 5, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 6, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, "end": 5, - "text": "foo" - } - }, - "length": 1, - "pos": 2, - "end": 5 + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + } + }, + "length": 1, + "pos": 2, + "end": 5 + } } }`); }); @@ -138,26 +148,31 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 14, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 13, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 14, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, - "end": 5, - "text": "foo" + "end": 13, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "type": { + "kind": "NumberKeyword", + "pos": 6, + "end": 13 + } }, - "type": { - "kind": "NumberKeyword", - "pos": 6, - "end": 13 - } - }, - "length": 1, - "pos": 2, - "end": 13 + "length": 1, + "pos": 2, + "end": 13 + } } }`); }); @@ -168,32 +183,37 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 11, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 5, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 11, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, - "end": 5, - "text": "foo" - } - }, - "1": { - "kind": "JSDocRecordMember", - "pos": 6, - "end": 10, - "name": { - "kind": "Identifier", + "end": 6, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + } + }, + "1": { + "kind": "PropertySignature", "pos": 6, "end": 10, - "text": "bar" - } - }, - "length": 2, - "pos": 2, - "end": 10 + "name": { + "kind": "Identifier", + "pos": 6, + "end": 10, + "text": "bar" + } + }, + "length": 2, + "pos": 2, + "end": 10 + } } }`); }); @@ -204,37 +224,42 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 19, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 13, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 19, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, - "end": 5, - "text": "foo" + "end": 14, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "type": { + "kind": "NumberKeyword", + "pos": 6, + "end": 13 + } }, - "type": { - "kind": "NumberKeyword", - "pos": 6, - "end": 13 - } - }, - "1": { - "kind": "JSDocRecordMember", - "pos": 14, - "end": 18, - "name": { - "kind": "Identifier", + "1": { + "kind": "PropertySignature", "pos": 14, "end": 18, - "text": "bar" - } - }, - "length": 2, - "pos": 2, - "end": 18 + "name": { + "kind": "Identifier", + "pos": 14, + "end": 18, + "text": "bar" + } + }, + "length": 2, + "pos": 2, + "end": 18 + } } }`); }); @@ -245,37 +270,42 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 19, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 5, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 19, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, - "end": 5, - "text": "foo" - } - }, - "1": { - "kind": "JSDocRecordMember", - "pos": 6, - "end": 18, - "name": { - "kind": "Identifier", - "pos": 6, - "end": 10, - "text": "bar" + "end": 6, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + } }, - "type": { - "kind": "NumberKeyword", - "pos": 11, - "end": 18 - } - }, - "length": 2, - "pos": 2, - "end": 18 + "1": { + "kind": "PropertySignature", + "pos": 6, + "end": 18, + "name": { + "kind": "Identifier", + "pos": 6, + "end": 10, + "text": "bar" + }, + "type": { + "kind": "NumberKeyword", + "pos": 11, + "end": 18 + } + }, + "length": 2, + "pos": 2, + "end": 18 + } } }`); }); @@ -286,42 +316,47 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 27, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 13, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 27, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, - "end": 5, - "text": "foo" + "end": 14, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "type": { + "kind": "NumberKeyword", + "pos": 6, + "end": 13 + } }, - "type": { - "kind": "NumberKeyword", - "pos": 6, - "end": 13 - } - }, - "1": { - "kind": "JSDocRecordMember", - "pos": 14, - "end": 26, - "name": { - "kind": "Identifier", + "1": { + "kind": "PropertySignature", "pos": 14, - "end": 18, - "text": "bar" + "end": 26, + "name": { + "kind": "Identifier", + "pos": 14, + "end": 18, + "text": "bar" + }, + "type": { + "kind": "NumberKeyword", + "pos": 19, + "end": 26 + } }, - "type": { - "kind": "NumberKeyword", - "pos": 19, - "end": 26 - } - }, - "length": 2, - "pos": 2, - "end": 26 + "length": 2, + "pos": 2, + "end": 26 + } } }`); }); @@ -332,22 +367,128 @@ namespace ts { "kind": "JSDocRecordType", "pos": 1, "end": 11, - "members": { - "0": { - "kind": "JSDocRecordMember", - "pos": 2, - "end": 10, - "name": { - "kind": "Identifier", + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 11, + "members": { + "0": { + "kind": "PropertySignature", "pos": 2, "end": 10, - "originalKeywordKind": "FunctionKeyword", - "text": "function" - } - }, - "length": 1, - "pos": 2, - "end": 10 + "name": { + "kind": "Identifier", + "pos": 2, + "end": 10, + "originalKeywordKind": "FunctionKeyword", + "text": "function" + } + }, + "length": 1, + "pos": 2, + "end": 10 + } + } +}`); + }); + + it("trailingCommaInRecordType", () => { + parsesCorrectly("{{a,}}", `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 5, + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 5, + "members": { + "0": { + "kind": "PropertySignature", + "pos": 2, + "end": 4, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 3, + "text": "a" + } + }, + "length": 1, + "pos": 2, + "end": 4 + } + } +}`); + }); + + it("callSignatureInRecordType", () => { + parsesCorrectly("{{(): number}}", `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 13, + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 13, + "members": { + "0": { + "kind": "CallSignature", + "pos": 2, + "end": 12, + "parameters": { + "length": 0, + "pos": 3, + "end": 3 + }, + "type": { + "kind": "NumberKeyword", + "pos": 5, + "end": 12 + } + }, + "length": 1, + "pos": 2, + "end": 12 + } + } +}`); + }); + + it("methodInRecordType", () => { + parsesCorrectly("{{foo(): number}}", `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 16, + "literal": { + "kind": "TypeLiteral", + "pos": 1, + "end": 16, + "members": { + "0": { + "kind": "MethodSignature", + "pos": 2, + "end": 15, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "parameters": { + "length": 0, + "pos": 6, + "end": 6 + }, + "type": { + "kind": "NumberKeyword", + "pos": 8, + "end": 15 + } + }, + "length": 1, + "pos": 2, + "end": 15 + } } }`); }); @@ -872,17 +1013,14 @@ namespace ts { } }`); }); - }); + + }); describe("parsesIncorrectly", () => { it("emptyType", () => { parsesIncorrectly("{}"); }); - it("trailingCommaInRecordType", () => { - parsesIncorrectly("{{a,}}"); - }); - it("unionTypeWithTrailingBar", () => { parsesIncorrectly("{(a|)}"); }); @@ -947,14 +1085,6 @@ namespace ts { parsesIncorrectly("{function(a: number)}"); }); - it("callSignatureInRecordType", () => { - parsesIncorrectly("{{(): number}}"); - }); - - it("methodInRecordType", () => { - parsesIncorrectly("{{foo(): number}}"); - }); - it("tupleTypeWithComma", () => { parsesIncorrectly( "{[,]}"); }); @@ -979,7 +1109,7 @@ namespace ts { Debug.fail("Comment has at least one diagnostic: " + comment.diagnostics[0].messageText); } - const result = toJsonString(comment.jsDocComment); + const result = toJsonString(comment.jsDoc); const expectedString = typeof expected === "string" ? expected @@ -990,7 +1120,7 @@ namespace ts { const chai = require("chai"); chai.config.showDiff = true; // Use deep equal to compare key value data instead of the two objects - chai.expect(JSON.parse(result)).deep.equal(JSON.parse(expectedString)); + chai.expect(JSON.parse(expectedString)).deep.equal(JSON.parse(result)); } else { assert.equal(result, expectedString); @@ -1028,10 +1158,6 @@ namespace ts { parsesIncorrectly("/*** */"); }); - it("asteriskAfterPreamble", () => { - parsesIncorrectly("/** * @type {number} */"); - }); - it("multipleTypes", () => { parsesIncorrectly( `/** @@ -1091,6 +1217,7 @@ namespace ts { "0": { "kind": "JSDocTypeTag", "pos": 8, + "comment": "", "end": 22, "atToken": { "kind": "AtToken", @@ -1112,7 +1239,8 @@ namespace ts { "pos": 15, "end": 21 } - } + }, + "comment": "" }, "length": 1, "pos": 8, @@ -1121,6 +1249,15 @@ namespace ts { }`); }); + it("asteriskAfterPreamble", () => { + parsesCorrectly("/** * @type {number} */", `{ + "comment": "* @type {number} ", + "end": 23, + "kind": "JSDocComment", + "pos": 0 +}`); + }); + it("noType", () => { parsesCorrectly( `/** @@ -1133,8 +1270,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTypeTag", + "comment": "", "pos": 8, - "end": 13, + "end": 14, "atToken": { "kind": "AtToken", "pos": 8, @@ -1149,7 +1287,7 @@ namespace ts { }, "length": 1, "pos": 8, - "end": 13 + "end": 14 } }`); }); @@ -1166,8 +1304,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocReturnTag", + "comment": "", "pos": 8, - "end": 15, + "end": 16, "atToken": { "kind": "AtToken", "pos": 8, @@ -1182,7 +1321,7 @@ namespace ts { }, "length": 1, "pos": 8, - "end": 15 + "end": 16 } }`); }); @@ -1199,6 +1338,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocTypeTag", + "comment": "", "pos": 8, "end": 22, "atToken": { @@ -1242,6 +1382,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocTypeTag", + "comment": "", "pos": 8, "end": 22, "atToken": { @@ -1285,6 +1426,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocReturnTag", + "comment": "", "pos": 8, "end": 24, "atToken": { @@ -1328,6 +1470,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocReturnTag", + "comment": "Description text follows", "pos": 8, "end": 24, "atToken": { @@ -1371,6 +1514,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocReturnTag", + "comment": "", "pos": 8, "end": 25, "atToken": { @@ -1414,6 +1558,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "", "pos": 8, "end": 29, "atToken": { @@ -1442,6 +1587,12 @@ namespace ts { "pos": 24, "end": 29, "text": "name1" + }, + "parameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" } }, "length": 1, @@ -1464,6 +1615,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "", "pos": 8, "end": 29, "atToken": { @@ -1492,10 +1644,17 @@ namespace ts { "pos": 24, "end": 29, "text": "name1" + }, + "parameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" } }, "1": { "kind": "JSDocParameterTag", + "comment": "", "pos": 34, "end": 55, "atToken": { @@ -1524,6 +1683,12 @@ namespace ts { "pos": 50, "end": 55, "text": "name2" + }, + "parameterName": { + "kind": "Identifier", + "pos": 50, + "end": 55, + "text": "name2" } }, "length": 2, @@ -1545,6 +1710,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "Description text follows", "pos": 8, "end": 29, "atToken": { @@ -1573,6 +1739,12 @@ namespace ts { "pos": 24, "end": 29, "text": "name1" + }, + "parameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" } }, "length": 1, @@ -1594,6 +1766,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "Description text follows", "pos": 8, "end": 31, "atToken": { @@ -1623,6 +1796,12 @@ namespace ts { "end": 30, "text": "name1" }, + "parameterName": { + "kind": "Identifier", + "pos": 25, + "end": 30, + "text": "name1" + }, "isBracketed": true }, "length": 1, @@ -1644,6 +1823,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "Description text follows", "pos": 8, "end": 36, "atToken": { @@ -1673,6 +1853,12 @@ namespace ts { "end": 31, "text": "name1" }, + "parameterName": { + "kind": "Identifier", + "pos": 26, + "end": 31, + "text": "name1" + }, "isBracketed": true }, "length": 1, @@ -1694,6 +1880,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "", "pos": 8, "end": 29, "atToken": { @@ -1722,11 +1909,56 @@ namespace ts { "pos": 24, "end": 29, "text": "name1" + }, + "parameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" } }, - "length": 1, - "pos": 8, - "end": 29 + "1": { + "atToken": { + "end": 31, + "kind": "AtToken", + "pos": 30 + }, + "comment": "", + "end": 51, + "kind": "JSDocParameterTag", + "parameterName": { + "end": 51, + "kind": "Identifier", + "pos": 46, + "text": "name2" + }, + "pos": 30, + "postParameterName": { + "end": 51, + "kind": "Identifier", + "pos": 46, + "text": "name2" + }, + "tagName": { + "end": 36, + "kind": "Identifier", + "pos": 31, + "text": "param" + }, + "typeExpression": { + "end": 45, + "kind": "JSDocTypeExpression", + "pos": 37, + "type": { + "end": 44, + "kind": "NumberKeyword", + "pos": 38 + } + } + }, + "end": 51, + "length": 2, + "pos": 8 } }`); }); @@ -1743,6 +1975,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "", "pos": 8, "end": 29, "atToken": { @@ -1762,6 +1995,12 @@ namespace ts { "end": 20, "text": "name1" }, + "parameterName": { + "kind": "Identifier", + "pos": 15, + "end": 20, + "text": "name1" + }, "typeExpression": { "kind": "JSDocTypeExpression", "pos": 21, @@ -1792,6 +2031,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "Description", "pos": 8, "end": 29, "atToken": { @@ -1811,6 +2051,12 @@ namespace ts { "end": 20, "text": "name1" }, + "parameterName": { + "kind": "Identifier", + "pos": 15, + "end": 20, + "text": "name1" + }, "typeExpression": { "kind": "JSDocTypeExpression", "pos": 21, @@ -1841,8 +2087,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTemplateTag", + "comment": "", "pos": 8, - "end": 19, + "end": 20, "atToken": { "kind": "AtToken", "pos": 8, @@ -1858,7 +2105,7 @@ namespace ts { "0": { "kind": "TypeParameter", "pos": 18, - "end": 19, + "end": 20, "name": { "kind": "Identifier", "pos": 18, @@ -1867,13 +2114,13 @@ namespace ts { } }, "length": 1, - "pos": 17, - "end": 19 + "pos": 18, + "end": 20 } }, "length": 1, "pos": 8, - "end": 19 + "end": 20 } }`); }); @@ -1890,8 +2137,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTemplateTag", + "comment": "", "pos": 8, - "end": 21, + "end": 22, "atToken": { "kind": "AtToken", "pos": 8, @@ -1918,7 +2166,7 @@ namespace ts { "1": { "kind": "TypeParameter", "pos": 20, - "end": 21, + "end": 22, "name": { "kind": "Identifier", "pos": 20, @@ -1927,13 +2175,13 @@ namespace ts { } }, "length": 2, - "pos": 17, - "end": 21 + "pos": 18, + "end": 22 } }, "length": 1, "pos": 8, - "end": 21 + "end": 22 } }`); }); @@ -1950,8 +2198,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTemplateTag", + "comment": "", "pos": 8, - "end": 22, + "end": 23, "atToken": { "kind": "AtToken", "pos": 8, @@ -1967,7 +2216,7 @@ namespace ts { "0": { "kind": "TypeParameter", "pos": 18, - "end": 19, + "end": 20, "name": { "kind": "Identifier", "pos": 18, @@ -1978,7 +2227,7 @@ namespace ts { "1": { "kind": "TypeParameter", "pos": 21, - "end": 22, + "end": 23, "name": { "kind": "Identifier", "pos": 21, @@ -1987,13 +2236,13 @@ namespace ts { } }, "length": 2, - "pos": 17, - "end": 22 + "pos": 18, + "end": 23 } }, "length": 1, "pos": 8, - "end": 22 + "end": 23 } }`); }); @@ -2010,8 +2259,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTemplateTag", + "comment": "", "pos": 8, - "end": 22, + "end": 23, "atToken": { "kind": "AtToken", "pos": 8, @@ -2038,7 +2288,7 @@ namespace ts { "1": { "kind": "TypeParameter", "pos": 21, - "end": 22, + "end": 23, "name": { "kind": "Identifier", "pos": 21, @@ -2047,13 +2297,13 @@ namespace ts { } }, "length": 2, - "pos": 17, - "end": 22 + "pos": 18, + "end": 23 } }, "length": 1, "pos": 8, - "end": 22 + "end": 23 } }`); }); @@ -2070,8 +2320,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTemplateTag", + "comment": "", "pos": 8, - "end": 23, + "end": 24, "atToken": { "kind": "AtToken", "pos": 8, @@ -2087,7 +2338,7 @@ namespace ts { "0": { "kind": "TypeParameter", "pos": 18, - "end": 19, + "end": 20, "name": { "kind": "Identifier", "pos": 18, @@ -2098,7 +2349,7 @@ namespace ts { "1": { "kind": "TypeParameter", "pos": 22, - "end": 23, + "end": 24, "name": { "kind": "Identifier", "pos": 22, @@ -2107,13 +2358,13 @@ namespace ts { } }, "length": 2, - "pos": 17, - "end": 23 + "pos": 18, + "end": 24 } }, "length": 1, "pos": 8, - "end": 23 + "end": 24 } }`); }); @@ -2130,8 +2381,9 @@ namespace ts { "tags": { "0": { "kind": "JSDocTemplateTag", + "comment": "Description of type parameters.", "pos": 8, - "end": 23, + "end": 24, "atToken": { "kind": "AtToken", "pos": 8, @@ -2147,7 +2399,7 @@ namespace ts { "0": { "kind": "TypeParameter", "pos": 18, - "end": 19, + "end": 20, "name": { "kind": "Identifier", "pos": 18, @@ -2158,7 +2410,7 @@ namespace ts { "1": { "kind": "TypeParameter", "pos": 22, - "end": 23, + "end": 24, "name": { "kind": "Identifier", "pos": 22, @@ -2167,13 +2419,13 @@ namespace ts { } }, "length": 2, - "pos": 17, - "end": 23 + "pos": 18, + "end": 24 } }, "length": 1, "pos": 8, - "end": 23 + "end": 24 } }`); }); @@ -2190,6 +2442,7 @@ namespace ts { "tags": { "0": { "kind": "JSDocParameterTag", + "comment": "", "pos": 8, "end": 18, "atToken": { @@ -2203,6 +2456,12 @@ namespace ts { "end": 14, "text": "param" }, + "parameterName": { + "kind": "Identifier", + "pos": 15, + "end": 18, + "text": "foo" + }, "preParameterName": { "kind": "Identifier", "pos": 15, @@ -2236,17 +2495,18 @@ namespace ts { "kind": "AtToken", "pos": 8 }, - "end": 97, + "comment": "", + "end": 98, "jsDocTypeLiteral": { - "end": 97, + "end": 98, "jsDocPropertyTags": [ { "atToken": { "end": 48, "kind": "AtToken", - "pos": 46 + "pos": 47 }, - "end": 69, + "end": 72, "kind": "JSDocPropertyTag", "name": { "end": 69, @@ -2254,7 +2514,7 @@ namespace ts { "pos": 66, "text": "age" }, - "pos": 46, + "pos": 47, "tagName": { "end": 56, "kind": "Identifier", @@ -2276,9 +2536,9 @@ namespace ts { "atToken": { "end": 75, "kind": "AtToken", - "pos": 73 + "pos": 74 }, - "end": 97, + "end": 98, "kind": "JSDocPropertyTag", "name": { "end": 97, @@ -2286,7 +2546,7 @@ namespace ts { "pos": 93, "text": "name" }, - "pos": 73, + "pos": 74, "tagName": { "end": 83, "kind": "Identifier", @@ -2309,11 +2569,11 @@ namespace ts { "atToken": { "end": 29, "kind": "AtToken", - "pos": 27 + "pos": 28 }, "end": 42, "kind": "JSDocTypeTag", - "pos": 27, + "pos": 28, "tagName": { "end": 33, "kind": "Identifier", @@ -2338,7 +2598,7 @@ namespace ts { } }, "kind": "JSDocTypeLiteral", - "pos": 23 + "pos": 26 }, "kind": "JSDocTypedefTag", "name": { @@ -2355,7 +2615,7 @@ namespace ts { "text": "typedef" } }, - "end": 97, + "end": 98, "length": 1, "pos": 8 } diff --git a/src/services/services.ts b/src/services/services.ts index c19eb487d75..812ae0643d8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -194,7 +194,7 @@ namespace ts { public end: number; public flags: NodeFlags; public parent: Node; - public jsDocComments: JSDocComment[]; + public jsDocComments: JSDoc[]; private _children: Node[]; constructor(kind: SyntaxKind, pos: number, end: number) { @@ -355,7 +355,7 @@ namespace ts { public end: number; public flags: NodeFlags; public parent: Node; - public jsDocComments: JSDocComment[]; + public jsDocComments: JSDoc[]; public __tokenTag: any; constructor(pos: number, end: number) { @@ -474,349 +474,48 @@ namespace ts { } function getJsDocCommentsFromDeclarations(declarations: Declaration[], name: string, canUseParsedParamTagComments: boolean) { + // 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 = []; - const docComments = getJsDocCommentsSeparatedByNewLines(); - ts.forEach(docComments, docComment => { - if (documentationComment.length) { - documentationComment.push(lineBreakPart()); + forEachUnique(declarations, declaration => { + const comments = getJSDocComments(declaration, /*checkParentVariableStatement*/ true); + if (!comments) { + return; + } + for (const comment of comments) { + if (comment) { + if (documentationComment.length) { + documentationComment.push(lineBreakPart()); + } + documentationComment.push(textPart(comment)); + } } - documentationComment.push(docComment); }); return documentationComment; + } - function getJsDocCommentsSeparatedByNewLines() { - const paramTag = "@param"; - const jsDocCommentParts: SymbolDisplayPart[] = []; - - ts.forEach(declarations, (declaration, indexOfDeclaration) => { - // Make sure we are collecting doc comment from declaration once, - // In case of 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 - if (indexOf(declarations, declaration) === indexOfDeclaration) { - const sourceFileOfDeclaration = getSourceFileOfNode(declaration); - // If it is parameter - try and get the jsDoc comment with @param tag from function declaration's jsDoc comments - if (canUseParsedParamTagComments && declaration.kind === SyntaxKind.Parameter) { - if ((declaration.parent.kind === SyntaxKind.FunctionExpression || declaration.parent.kind === SyntaxKind.ArrowFunction) && - declaration.parent.parent.kind === SyntaxKind.VariableDeclaration) { - addCommentParts(declaration.parent.parent.parent, sourceFileOfDeclaration, getCleanedParamJsDocComment); - } - addCommentParts(declaration.parent, sourceFileOfDeclaration, getCleanedParamJsDocComment); - } - - // If this is left side of dotted module declaration, there is no doc comments associated with this node - if (declaration.kind === SyntaxKind.ModuleDeclaration && (declaration).body && (declaration).body.kind === SyntaxKind.ModuleDeclaration) { - return; - } - - if ((declaration.kind === SyntaxKind.FunctionExpression || declaration.kind === SyntaxKind.ArrowFunction) && - declaration.parent.kind === SyntaxKind.VariableDeclaration) { - addCommentParts(declaration.parent.parent, sourceFileOfDeclaration, getCleanedJsDocComment); - } - - // If this is dotted module name, get the doc comments from the parent - while (declaration.kind === SyntaxKind.ModuleDeclaration && declaration.parent.kind === SyntaxKind.ModuleDeclaration) { - declaration = declaration.parent; - } - addCommentParts(declaration.kind === SyntaxKind.VariableDeclaration ? declaration.parent.parent : declaration, - sourceFileOfDeclaration, - getCleanedJsDocComment); - - if (declaration.kind === SyntaxKind.VariableDeclaration) { - const init = (declaration as VariableDeclaration).initializer; - if (init && (init.kind === SyntaxKind.FunctionExpression || init.kind === SyntaxKind.ArrowFunction)) { - // Get the cleaned js doc comment text from the initializer - addCommentParts(init, sourceFileOfDeclaration, getCleanedJsDocComment); - } - } - } - }); - - return jsDocCommentParts; - - function addCommentParts(commented: Node, - sourceFileOfDeclaration: SourceFile, - getCommentPart: (pos: number, end: number, file: SourceFile) => SymbolDisplayPart[]): void { - const ranges = getJsDocCommentTextRange(commented, sourceFileOfDeclaration); - // Get the cleaned js doc comment text from the declaration - ts.forEach(ranges, jsDocCommentTextRange => { - const cleanedComment = getCommentPart(jsDocCommentTextRange.pos, jsDocCommentTextRange.end, sourceFileOfDeclaration); - if (cleanedComment) { - addRange(jsDocCommentParts, cleanedComment); - } - }); - } - - function getJsDocCommentTextRange(node: Node, sourceFile: SourceFile): TextRange[] { - return ts.map(getJsDocComments(node, sourceFile), - jsDocComment => { - return { - pos: jsDocComment.pos + "/*".length, // Consume /* from the comment - end: jsDocComment.end - "*/".length // Trim off comment end indicator - }; - }); - } - - function consumeWhiteSpacesOnTheLine(pos: number, end: number, sourceFile: SourceFile, maxSpacesToRemove?: number) { - if (maxSpacesToRemove !== undefined) { - end = Math.min(end, pos + maxSpacesToRemove); - } - - for (; pos < end; pos++) { - const ch = sourceFile.text.charCodeAt(pos); - if (!isWhiteSpaceSingleLine(ch)) { - return pos; - } - } - - return end; - } - - function consumeLineBreaks(pos: number, end: number, sourceFile: SourceFile) { - while (pos < end && isLineBreak(sourceFile.text.charCodeAt(pos))) { - pos++; - } - - return pos; - } - - function isName(pos: number, end: number, sourceFile: SourceFile, name: string) { - return pos + name.length < end && - sourceFile.text.substr(pos, name.length) === name && - isWhiteSpace(sourceFile.text.charCodeAt(pos + name.length)); - } - - function isParamTag(pos: number, end: number, sourceFile: SourceFile) { - // If it is @param tag - return isName(pos, end, sourceFile, paramTag); - } - - function pushDocCommentLineText(docComments: SymbolDisplayPart[], text: string, blankLineCount: number) { - // Add the empty lines in between texts - while (blankLineCount) { - blankLineCount--; - docComments.push(textPart("")); - } - - docComments.push(textPart(text)); - } - - function getCleanedJsDocComment(pos: number, end: number, sourceFile: SourceFile) { - let spacesToRemoveAfterAsterisk: number; - const docComments: SymbolDisplayPart[] = []; - let blankLineCount = 0; - let isInParamTag = false; - - while (pos < end) { - let docCommentTextOfLine = ""; - // First consume leading white space - pos = consumeWhiteSpacesOnTheLine(pos, end, sourceFile); - - // If the comment starts with '*' consume the spaces on this line - if (pos < end && sourceFile.text.charCodeAt(pos) === CharacterCodes.asterisk) { - const lineStartPos = pos + 1; - pos = consumeWhiteSpacesOnTheLine(pos + 1, end, sourceFile, spacesToRemoveAfterAsterisk); - - // Set the spaces to remove after asterisk as margin if not already set - if (spacesToRemoveAfterAsterisk === undefined && pos < end && !isLineBreak(sourceFile.text.charCodeAt(pos))) { - spacesToRemoveAfterAsterisk = pos - lineStartPos; - } - } - else if (spacesToRemoveAfterAsterisk === undefined) { - spacesToRemoveAfterAsterisk = 0; - } - - // Analyze text on this line - while (pos < end && !isLineBreak(sourceFile.text.charCodeAt(pos))) { - const ch = sourceFile.text.charAt(pos); - if (ch === "@") { - // If it is @param tag - if (isParamTag(pos, end, sourceFile)) { - isInParamTag = true; - pos += paramTag.length; - continue; - } - else { - isInParamTag = false; - } - } - - // Add the ch to doc text if we arent in param tag - if (!isInParamTag) { - docCommentTextOfLine += ch; - } - - // Scan next character - pos++; - } - - // Continue with next line - pos = consumeLineBreaks(pos, end, sourceFile); - if (docCommentTextOfLine) { - pushDocCommentLineText(docComments, docCommentTextOfLine, blankLineCount); - blankLineCount = 0; - } - else if (!isInParamTag && docComments.length) { - // This is blank line when there is text already parsed - blankLineCount++; - } - } - - return docComments; - } - - function getCleanedParamJsDocComment(pos: number, end: number, sourceFile: SourceFile) { - let paramHelpStringMargin: number; - const paramDocComments: SymbolDisplayPart[] = []; - while (pos < end) { - if (isParamTag(pos, end, sourceFile)) { - let blankLineCount = 0; - let recordedParamTag = false; - // Consume leading spaces - pos = consumeWhiteSpaces(pos + paramTag.length); - if (pos >= end) { - break; - } - - // Ignore type expression - if (sourceFile.text.charCodeAt(pos) === CharacterCodes.openBrace) { - pos++; - for (let curlies = 1; pos < end; pos++) { - const charCode = sourceFile.text.charCodeAt(pos); - - // { character means we need to find another } to match the found one - if (charCode === CharacterCodes.openBrace) { - curlies++; - continue; - } - - // } char - if (charCode === CharacterCodes.closeBrace) { - curlies--; - if (curlies === 0) { - // We do not have any more } to match the type expression is ignored completely - pos++; - break; - } - else { - // there are more { to be matched with } - continue; - } - } - - // Found start of another tag - if (charCode === CharacterCodes.at) { - break; - } - } - - // Consume white spaces - pos = consumeWhiteSpaces(pos); - if (pos >= end) { - break; - } - } - - // Parameter name - if (isName(pos, end, sourceFile, name)) { - // Found the parameter we are looking for consume white spaces - pos = consumeWhiteSpaces(pos + name.length); - if (pos >= end) { - break; - } - - let paramHelpString = ""; - const firstLineParamHelpStringPos = pos; - while (pos < end) { - const ch = sourceFile.text.charCodeAt(pos); - - // at line break, set this comment line text and go to next line - if (isLineBreak(ch)) { - if (paramHelpString) { - pushDocCommentLineText(paramDocComments, paramHelpString, blankLineCount); - paramHelpString = ""; - blankLineCount = 0; - recordedParamTag = true; - } - else if (recordedParamTag) { - blankLineCount++; - } - - // Get the pos after cleaning start of the line - setPosForParamHelpStringOnNextLine(firstLineParamHelpStringPos); - continue; - } - - // Done scanning param help string - next tag found - if (ch === CharacterCodes.at) { - break; - } - - paramHelpString += sourceFile.text.charAt(pos); - - // Go to next character - pos++; - } - - // If there is param help text, add it top the doc comments - if (paramHelpString) { - pushDocCommentLineText(paramDocComments, paramHelpString, blankLineCount); - } - paramHelpStringMargin = undefined; - } - - // If this is the start of another tag, continue with the loop in search of param tag with symbol name - if (sourceFile.text.charCodeAt(pos) === CharacterCodes.at) { - continue; - } - } - - // Next character - pos++; - } - - return paramDocComments; - - function consumeWhiteSpaces(pos: number) { - while (pos < end && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { - pos++; - } - - return pos; - } - - function setPosForParamHelpStringOnNextLine(firstLineParamHelpStringPos: number) { - // Get the pos after consuming line breaks - pos = consumeLineBreaks(pos, end, sourceFile); - if (pos >= end) { - return; - } - - if (paramHelpStringMargin === undefined) { - paramHelpStringMargin = sourceFile.getLineAndCharacterOfPosition(firstLineParamHelpStringPos).character; - } - - // Now consume white spaces max - const startOfLinePos = pos; - pos = consumeWhiteSpacesOnTheLine(pos, end, sourceFile, paramHelpStringMargin); - if (pos >= end) { - return; - } - - const consumedSpaces = pos - startOfLinePos; - if (consumedSpaces < paramHelpStringMargin) { - const ch = sourceFile.text.charCodeAt(pos); - if (ch === CharacterCodes.asterisk) { - // Consume more spaces after asterisk - pos = consumeWhiteSpacesOnTheLine(pos + 1, end, sourceFile, paramHelpStringMargin - consumedSpaces - 1); - } + /** + * 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. + */ + export 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; } class TypeObject implements Type { @@ -7498,9 +7197,9 @@ namespace ts { // See if this is a doc comment. If so, we'll classify certain portions of it // specially. const docCommentAndDiagnostics = parseIsolatedJSDocComment(sourceFile.text, start, width); - if (docCommentAndDiagnostics && docCommentAndDiagnostics.jsDocComment) { - docCommentAndDiagnostics.jsDocComment.parent = token; - classifyJSDocComment(docCommentAndDiagnostics.jsDocComment); + if (docCommentAndDiagnostics && docCommentAndDiagnostics.jsDoc) { + docCommentAndDiagnostics.jsDoc.parent = token; + classifyJSDocComment(docCommentAndDiagnostics.jsDoc); return; } } @@ -7513,22 +7212,22 @@ namespace ts { pushClassification(start, width, ClassificationType.comment); } - function classifyJSDocComment(docComment: JSDocComment) { + function classifyJSDocComment(docComment: JSDoc) { let pos = docComment.pos; + if (docComment.tags) { + for (const tag of docComment.tags) { + // As we walk through each tag, classify the portion of text from the end of + // the last tag (or the start of the entire doc comment) as 'comment'. + if (tag.pos !== pos) { + pushCommentRange(pos, tag.pos - pos); + } - for (const tag of docComment.tags) { - // As we walk through each tag, classify the portion of text from the end of - // the last tag (or the start of the entire doc comment) as 'comment'. - if (tag.pos !== pos) { - pushCommentRange(pos, tag.pos - pos); - } + pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); + pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); - pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); - pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); + pos = tag.tagName.end; - pos = tag.tagName.end; - - switch (tag.kind) { + switch (tag.kind) { case SyntaxKind.JSDocParameterTag: processJSDocParameterTag(tag); break; @@ -7541,9 +7240,10 @@ namespace ts { case SyntaxKind.JSDocReturnTag: processElement((tag).typeExpression); break; - } + } - pos = tag.end; + pos = tag.end; + } } if (pos !== docComment.end) { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 9b3469643ba..4ceecb4bf50 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -569,9 +569,11 @@ namespace ts { if (node) { if (node.jsDocComments) { for (const jsDocComment of node.jsDocComments) { - for (const tag of jsDocComment.tags) { - if (tag.pos <= position && position <= tag.end) { - return tag; + if (jsDocComment.tags) { + for (const tag of jsDocComment.tags) { + if (tag.pos <= position && position <= tag.end) { + return tag; + } } } } @@ -946,4 +948,4 @@ namespace ts { } return { configJsonObject, diagnostics }; } -} \ No newline at end of file +}