diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index c021b87d1c470..475216f6e1c75 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1026,7 +1026,10 @@ namespace ts { let hasDeprecatedTag = false; function addJSDocComment<T extends HasJSDoc>(node: T): T { Debug.assert(!node.jsDoc); // Should only be called once per node - const jsDoc = mapDefined(getJSDocCommentRanges(node, sourceText), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); + const ranges = getJSDocCommentRanges(node, sourceText); + const jsDoc = ranges?.every(r => isTripleSlashComment(r, sourceText)) + ? JSDocParser.parseTripleSlashes(node, ranges) + : mapDefined(ranges, comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); if (jsDoc.length) node.jsDoc = jsDoc; if (hasDeprecatedTag) { hasDeprecatedTag = false; @@ -7158,6 +7161,26 @@ namespace ts { return jsDoc ? { jsDoc, diagnostics } : undefined; } + export function parseTripleSlashes(parent: HasJSDoc, comments: CommentRange[]) { + const saveToken = currentToken; + const saveParseDiagnosticsLength = parseDiagnostics.length; + const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; + + const comment = doInsideOfContext(NodeFlags.JSDoc, () => parseJSDocCommentWorker(comments, /*length*/ undefined)); + setParent(comment, parent); + + if (contextFlags & NodeFlags.JavaScriptFile) { + if (!jsDocDiagnostics) { + jsDocDiagnostics = []; + } + jsDocDiagnostics.push(...parseDiagnostics); + } + currentToken = saveToken; + parseDiagnostics.length = saveParseDiagnosticsLength; + parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; + return comment ? [comment] : []; + } + export function parseJSDocComment(parent: HasJSDoc, start: number, length: number): JSDoc | undefined { const saveToken = currentToken; const saveParseDiagnosticsLength = parseDiagnostics.length; @@ -7191,8 +7214,43 @@ namespace ts { CallbackParameter = 1 << 2, } - function parseJSDocCommentWorker(start = 0, length: number | undefined): JSDoc | undefined { - const content = sourceText; + function parseJSDocCommentWorker(startOrRanges: number | CommentRange[] = 0, length: number | undefined): JSDoc | undefined { + // const isTripleSlash = Array.isArray(startOrRanges); + // TODO: Probably should save a boolean isTripleSlash at the beginning and make all the nested functions change their behaviour. + const content = sourceText; // TODO: Why alias this? + const comments: string[] = []; + let tags: JSDocTag[]; + let tagsPos: number; + let tagsEnd: number; + if (Array.isArray(startOrRanges)) { + if (!startOrRanges.length) return undefined; + const ranges = startOrRanges; + let currentTag: JSDocTag | undefined; // TODO: Probably can use tags + for (const { pos: start, end } of ranges) { + const length = end - start; + scanner.scanRange(start + 3, length - 3, () => { + while (nextTokenJSDoc() !== SyntaxKind.EndOfFileToken) { + if (token() === SyntaxKind.AtToken) { + addTag(currentTag); + currentTag = parseTag(0); + } + else { + if (currentTag) { + // this doesn't update currentTag.end, which will cause problems later + // I think that parseXTag will have to return unfinished tags or something. + (currentTag as any).comment = (currentTag.comment || "") + scanner.getTokenText() + } + else { + comments.push(scanner.getTokenText()); + } + } + } + }); + } + return createJSDocComment(ranges[0].pos, ranges[ranges.length - 1].end); + } + + const start = startOrRanges; const end = length === undefined ? content.length : start + length; length = end - start; @@ -7205,10 +7263,6 @@ namespace ts { return undefined; } - let tags: JSDocTag[]; - let tagsPos: number; - let tagsEnd: number; - const comments: string[] = []; // + 3 for leading /**, - 5 in total for /** */ return scanner.scanRange(start + 3, length - 5, () => { @@ -7292,7 +7346,7 @@ namespace ts { } removeLeadingNewlines(comments); removeTrailingWhitespace(comments); - return createJSDocComment(); + return createJSDocComment(start, end); }); function removeLeadingNewlines(comments: string[]) { @@ -7307,7 +7361,7 @@ namespace ts { } } - function createJSDocComment(): JSDoc { + function createJSDocComment(start: number, end: number): JSDoc { const comment = comments.length ? comments.join("") : undefined; const tagsArray = tags && createNodeArray(tags, tagsPos, tagsEnd); return finishNode(factory.createJSDocComment(comment, tagsArray), start, end); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 77ec82ffef760..2cd3ca63454c6 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1745,6 +1745,9 @@ namespace ts { case CharacterCodes.slash: // Single-line comment if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { + if (text.charCodeAt(pos + 2) === CharacterCodes.slash) { + tokenFlags |= TokenFlags.PrecedingJSDocComment; + } pos += 2; while (pos < end) { @@ -1770,10 +1773,10 @@ namespace ts { } // Multi-line comment if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { - pos += 2; - if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== CharacterCodes.slash) { + if (text.charCodeAt(pos + 2) === CharacterCodes.asterisk && text.charCodeAt(pos + 3) !== CharacterCodes.slash) { tokenFlags |= TokenFlags.PrecedingJSDocComment; } + pos += 2; let commentClosed = false; let lastLineStart = tokenPos; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index d7c00dbcc14c9..4a783ebd6e4e2 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1094,6 +1094,10 @@ namespace ts { return node.kind !== SyntaxKind.JsxText ? getLeadingCommentRanges(sourceFileOfNode.text, node.pos) : undefined; } + export function isTripleSlashComment(comment: CommentRange, text: string) { + return text.charCodeAt(comment.pos + 1) === CharacterCodes.slash && text.charCodeAt(comment.pos + 2) === CharacterCodes.slash; + } + export function getJSDocCommentRanges(node: Node, text: string) { const commentRanges = (node.kind === SyntaxKind.Parameter || node.kind === SyntaxKind.TypeParameter || @@ -1102,11 +1106,12 @@ namespace ts { node.kind === SyntaxKind.ParenthesizedExpression) ? concatenate(getTrailingCommentRanges(text, node.pos), getLeadingCommentRanges(text, node.pos)) : getLeadingCommentRanges(text, node.pos); - // True if the comment starts with '/**' but not if it is '/**/' + // True if the comment starts with '///' or '/**' but not if it is '/**/' return filter(commentRanges, comment => text.charCodeAt(comment.pos + 1) === CharacterCodes.asterisk && text.charCodeAt(comment.pos + 2) === CharacterCodes.asterisk && - text.charCodeAt(comment.pos + 3) !== CharacterCodes.slash); + text.charCodeAt(comment.pos + 3) !== CharacterCodes.slash + || isTripleSlashComment(comment, text) && !isRecognizedTripleSlashComment(text, comment.pos, comment.end)); } export const fullTripleSlashReferencePathRegEx = /^(\/\/\/\s*<reference\s+path\s*=\s*)('|")(.+?)\2.*?\/>/; diff --git a/tests/baselines/reference/tripleSlashJsdoc.symbols b/tests/baselines/reference/tripleSlashJsdoc.symbols new file mode 100644 index 0000000000000..8f5dca5a63b61 --- /dev/null +++ b/tests/baselines/reference/tripleSlashJsdoc.symbols @@ -0,0 +1,40 @@ +=== tests/cases/conformance/jsdoc/tripleSlash.js === +/// @type {number} - TODO this is still skipped for some reason +var x; +>x : Symbol(x, Decl(tripleSlash.js, 1, 3)) + +/// Adds one +/// @param {number} n - this is a long, +/// multiline comment +/// +/// @return {number} +function add1(n) { +>add1 : Symbol(add1, Decl(tripleSlash.js, 1, 6)) +>n : Symbol(n, Decl(tripleSlash.js, 8, 14)) + + return n + 1 +>n : Symbol(n, Decl(tripleSlash.js, 8, 14)) +} + +// Should be the same + +/** Adds one + * @param {number} n - this is a long, + * multiline comment + * + * @return {number} +*/ +function add2(n) { +>add2 : Symbol(add2, Decl(tripleSlash.js, 10, 1)) +>n : Symbol(n, Decl(tripleSlash.js, 20, 14)) + + return n + 1 +>n : Symbol(n, Decl(tripleSlash.js, 20, 14)) +} + +/// I documented this const +const documented = "" +>documented : Symbol(documented, Decl(tripleSlash.js, 25, 5)) + + + diff --git a/tests/baselines/reference/tripleSlashJsdoc.types b/tests/baselines/reference/tripleSlashJsdoc.types new file mode 100644 index 0000000000000..8a58c3170104b --- /dev/null +++ b/tests/baselines/reference/tripleSlashJsdoc.types @@ -0,0 +1,45 @@ +=== tests/cases/conformance/jsdoc/tripleSlash.js === +/// @type {number} - TODO this is still skipped for some reason +var x; +>x : number + +/// Adds one +/// @param {number} n - this is a long, +/// multiline comment +/// +/// @return {number} +function add1(n) { +>add1 : (n: number) => number +>n : number + + return n + 1 +>n + 1 : number +>n : number +>1 : 1 +} + +// Should be the same + +/** Adds one + * @param {number} n - this is a long, + * multiline comment + * + * @return {number} +*/ +function add2(n) { +>add2 : (n: number) => number +>n : number + + return n + 1 +>n + 1 : number +>n : number +>1 : 1 +} + +/// I documented this const +const documented = "" +>documented : "" +>"" : "" + + + diff --git a/tests/cases/conformance/jsdoc/tripleSlashJsdoc.ts b/tests/cases/conformance/jsdoc/tripleSlashJsdoc.ts new file mode 100644 index 0000000000000..991afd8a483da --- /dev/null +++ b/tests/cases/conformance/jsdoc/tripleSlashJsdoc.ts @@ -0,0 +1,34 @@ +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @noEmit: true +// @filename: tripleSlash.js + +/// @type {number} - TODO this is still skipped for some reason +var x; + +/// Adds one +/// @param {number} n - this is a long, +/// multiline comment +/// +/// @return {number} +function add1(n) { + return n + 1 +} + +// Should be the same + +/** Adds one + * @param {number} n - this is a long, + * multiline comment + * + * @return {number} +*/ +function add2(n) { + return n + 1 +} + +/// I documented this const +const documented = "" + + diff --git a/tests/cases/fourslash/commentsCommentParsing.ts b/tests/cases/fourslash/commentsCommentParsing.ts index 4731194ba697c..755a1d1cc6332 100644 --- a/tests/cases/fourslash/commentsCommentParsing.ts +++ b/tests/cases/fourslash/commentsCommentParsing.ts @@ -202,7 +202,7 @@ ////class NoQuic/*50*/kInfoClass { ////} -verify.signatureHelp({ marker: "1", docComment: "" }); +verify.signatureHelp({ marker: "1", docComment: " This is simple /// comments" }); verify.quickInfoAt("1q", "function simple(): void"); verify.signatureHelp({ marker: "2", docComment: "" }); diff --git a/tests/cases/fourslash/signatureHelpTripleSlashJSDoc.ts b/tests/cases/fourslash/signatureHelpTripleSlashJSDoc.ts new file mode 100644 index 0000000000000..23feb8652dc76 --- /dev/null +++ b/tests/cases/fourslash/signatureHelpTripleSlashJSDoc.ts @@ -0,0 +1,18 @@ +/// <reference path="fourslash.ts" /> + +// @allowJs: true +// @checkJs: true + +// @Filename: test.js +//// /// Adds one +//// /// @param {number} n - this is a long, +//// /// multiline comment +//// /// +//// /// @return {number} +//// function add1(/*2*/n) { +//// return n + 1 +//// } +//// add1/*1*/(12) + +verify.quickInfoAt('1', 'function add1(n: number): number', ' Adds one ') +verify.quickInfoAt('2', '(parameter) n: number', '- this is a long, multiline comment ')