From 795c53a786c01e1ae0fb7b73c62af9df1870b26c Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:45:40 -0700 Subject: [PATCH] JSDoc comment parsing supports multiline backticks Previously, backticks in JSDoc ended at the end of a line. This PR changes that so that backticks continue parsing until another backtick. Note that triple backticks and single backticks are treated the same way. The change is already very clunky, and I haven't thought about how much clunkier it would be to track backtick history in the parser. --- src/compiler/parser.ts | 23 +++++++++++--- .../jsdocParseMatchingBackticks.errors.txt | 31 +++++++++++++++++++ .../jsdocParseMatchingBackticks.symbols | 18 ++++++++++- .../jsdocParseMatchingBackticks.types | 19 +++++++++++- .../jsdoc/jsdocParseMatchingBackticks.ts | 12 ++++++- .../fourslash/quickInfoJSDocBackticks.ts | 11 ++++--- 6 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 tests/baselines/reference/jsdocParseMatchingBackticks.errors.txt diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index dbcffe1680ee9..7da0f03008a39 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -8123,6 +8123,8 @@ namespace ts { SawAsterisk, SavingComments, SavingBackticks, // NOTE: Only used when parsing tag comments + SavingBackticksBeginningOfLine, + SavingBackticksSawAsterisk, } const enum PropertyLikeParse { @@ -8433,13 +8435,13 @@ namespace ts { loop: while (true) { switch (tok) { case SyntaxKind.NewLineTrivia: - state = JSDocState.BeginningOfLine; + state = (state as JSDocState) === JSDocState.SavingBackticks || (state as JSDocState) === JSDocState.SavingBackticksBeginningOfLine || (state as JSDocState) === JSDocState.SavingBackticksSawAsterisk ? JSDocState.SavingBackticksBeginningOfLine : JSDocState.BeginningOfLine; // don't use pushComment here because we want to keep the margin unchanged comments.push(scanner.getTokenText()); indent = 0; break; case SyntaxKind.AtToken: - if (state === JSDocState.SavingBackticks + if (state === JSDocState.SavingBackticks || state === JSDocState.SavingBackticksBeginningOfLine || state === JSDocState.SavingBackticksSawAsterisk || state === JSDocState.SavingComments && (!previousWhitespace || lookAhead(isNextJSDocTokenWhitespace))) { // @ doesn't start a new tag inside ``, and inside a comment, only after whitespace or not before whitespace comments.push(scanner.getTokenText()); @@ -8464,7 +8466,7 @@ namespace ts { } break; case SyntaxKind.OpenBraceToken: - state = JSDocState.SavingComments; + state = (state as JSDocState) === JSDocState.SavingBackticks || (state as JSDocState) === JSDocState.SavingBackticksBeginningOfLine || (state as JSDocState) === JSDocState.SavingBackticksSawAsterisk ? JSDocState.SavingBackticks : JSDocState.SavingComments; const commentEnd = scanner.getStartPos(); const linkStart = scanner.getTextPos() - 1; const link = parseJSDocLink(linkStart); @@ -8479,7 +8481,7 @@ namespace ts { } break; case SyntaxKind.BacktickToken: - if (state === JSDocState.SavingBackticks) { + if (state === JSDocState.SavingBackticks || state === JSDocState.SavingBackticksBeginningOfLine || state === JSDocState.SavingBackticksSawAsterisk) { state = JSDocState.SavingComments; } else { @@ -8494,11 +8496,22 @@ namespace ts { indent += 1; break; } + if (state === JSDocState.SavingBackticksBeginningOfLine) { + // leading asterisks start recording on the *next* (non-whitespace) token + state = JSDocState.SavingBackticksSawAsterisk; + indent += 1; + break; + } // record the * as a comment // falls through default: if (state !== JSDocState.SavingBackticks) { - state = JSDocState.SavingComments; // leading identifiers start recording as well + if (state === JSDocState.SavingBackticksBeginningOfLine || state === JSDocState.SavingBackticksSawAsterisk) { + state = JSDocState.SavingBackticks + } + else { + state = JSDocState.SavingComments; // leading identifiers start recording as well + } } pushComment(scanner.getTokenText()); break; diff --git a/tests/baselines/reference/jsdocParseMatchingBackticks.errors.txt b/tests/baselines/reference/jsdocParseMatchingBackticks.errors.txt new file mode 100644 index 0000000000000..6d78551a3074c --- /dev/null +++ b/tests/baselines/reference/jsdocParseMatchingBackticks.errors.txt @@ -0,0 +1,31 @@ +tests/cases/conformance/jsdoc/jsdocParseMatchingBackticks.js(22,22): error TS7006: Parameter 'gamma' implicitly has an 'any' type. + + +==== tests/cases/conformance/jsdoc/jsdocParseMatchingBackticks.js (1 errors) ==== + /** + * `@param` initial at-param is OK in title comment + * @param {string} x hi there `@param` + * @param {string} y hi there @ * param + * this is the margin + * so we'll drop everything before it + `@param` @param {string} z hello??? + * `@param` @param {string} alpha hello??? + * `@ * param` @param {string} beta hello??? + * @param {string} gamma + */ + export function f(x, y, z, alpha, beta, gamma) { + return x + y + z + alpha + beta + gamma + } + /** + * Unmatched backticks keep going past newlines + * @param {string} y hi there `@ * param + * this is the margin + * so we'll drop everything before it + * @param {string} gamma + */ + export function g(y, gamma) { + ~~~~~ +!!! error TS7006: Parameter 'gamma' implicitly has an 'any' type. + return y + gamma + } + \ No newline at end of file diff --git a/tests/baselines/reference/jsdocParseMatchingBackticks.symbols b/tests/baselines/reference/jsdocParseMatchingBackticks.symbols index 83bd81113aa45..d25a7ebbb1ca2 100644 --- a/tests/baselines/reference/jsdocParseMatchingBackticks.symbols +++ b/tests/baselines/reference/jsdocParseMatchingBackticks.symbols @@ -2,7 +2,7 @@ /** * `@param` initial at-param is OK in title comment * @param {string} x hi there `@param` - * @param {string} y hi there `@ * param + * @param {string} y hi there @ * param * this is the margin * so we'll drop everything before it `@param` @param {string} z hello??? @@ -27,4 +27,20 @@ export function f(x, y, z, alpha, beta, gamma) { >beta : Symbol(beta, Decl(jsdocParseMatchingBackticks.js, 11, 33)) >gamma : Symbol(gamma, Decl(jsdocParseMatchingBackticks.js, 11, 39)) } +/** + * Unmatched backticks keep going past newlines + * @param {string} y hi there `@ * param + * this is the margin + * so we'll drop everything before it + * @param {string} gamma + */ +export function g(y, gamma) { +>g : Symbol(g, Decl(jsdocParseMatchingBackticks.js, 13, 1)) +>y : Symbol(y, Decl(jsdocParseMatchingBackticks.js, 21, 18)) +>gamma : Symbol(gamma, Decl(jsdocParseMatchingBackticks.js, 21, 20)) + + return y + gamma +>y : Symbol(y, Decl(jsdocParseMatchingBackticks.js, 21, 18)) +>gamma : Symbol(gamma, Decl(jsdocParseMatchingBackticks.js, 21, 20)) +} diff --git a/tests/baselines/reference/jsdocParseMatchingBackticks.types b/tests/baselines/reference/jsdocParseMatchingBackticks.types index 8127e836df419..42f10e8ea5b6d 100644 --- a/tests/baselines/reference/jsdocParseMatchingBackticks.types +++ b/tests/baselines/reference/jsdocParseMatchingBackticks.types @@ -2,7 +2,7 @@ /** * `@param` initial at-param is OK in title comment * @param {string} x hi there `@param` - * @param {string} y hi there `@ * param + * @param {string} y hi there @ * param * this is the margin * so we'll drop everything before it `@param` @param {string} z hello??? @@ -32,4 +32,21 @@ export function f(x, y, z, alpha, beta, gamma) { >beta : string >gamma : string } +/** + * Unmatched backticks keep going past newlines + * @param {string} y hi there `@ * param + * this is the margin + * so we'll drop everything before it + * @param {string} gamma + */ +export function g(y, gamma) { +>g : (y: string, gamma: any) => string +>y : string +>gamma : any + + return y + gamma +>y + gamma : string +>y : string +>gamma : any +} diff --git a/tests/cases/conformance/jsdoc/jsdocParseMatchingBackticks.ts b/tests/cases/conformance/jsdoc/jsdocParseMatchingBackticks.ts index 942ee98d52fb2..0a0c1d5406a3d 100644 --- a/tests/cases/conformance/jsdoc/jsdocParseMatchingBackticks.ts +++ b/tests/cases/conformance/jsdoc/jsdocParseMatchingBackticks.ts @@ -7,7 +7,7 @@ /** * `@param` initial at-param is OK in title comment * @param {string} x hi there `@param` - * @param {string} y hi there `@ * param + * @param {string} y hi there @ * param * this is the margin * so we'll drop everything before it `@param` @param {string} z hello??? @@ -18,3 +18,13 @@ export function f(x, y, z, alpha, beta, gamma) { return x + y + z + alpha + beta + gamma } +/** + * Unmatched backticks keep going past newlines + * @param {string} y hi there `@ * param + * this is the margin + * so we'll drop everything before it + * @param {string} gamma + */ +export function g(y, gamma) { + return y + gamma +} diff --git a/tests/cases/fourslash/quickInfoJSDocBackticks.ts b/tests/cases/fourslash/quickInfoJSDocBackticks.ts index 16ddce5a939cb..9610e1895e615 100644 --- a/tests/cases/fourslash/quickInfoJSDocBackticks.ts +++ b/tests/cases/fourslash/quickInfoJSDocBackticks.ts @@ -11,15 +11,18 @@ //// * @param {string} x hi there `@param` //// * @param {string} y hi there `@ * param //// * this is the margin +//// * @param {string} z OOPS, unclosed backtick! //// */ -////export function f(x, y) { -//// return x/*x*/ + y/*y*/ +////export function f(x, y, z) { +//// return x/*x*/ + y/*y*/ + z/*z*/ ////} ////f/*f*/ goTo.marker("f"); -verify.quickInfoIs("function f(x: string, y: string): string", "`@param` initial at-param is OK in title comment"); +verify.quickInfoIs("function f(x: string, y: string, z: any): string", "`@param` initial at-param is OK in title comment"); goTo.marker("x"); verify.quickInfoIs("(parameter) x: string", "hi there `@param`"); goTo.marker("y"); -verify.quickInfoIs("(parameter) y: string", "hi there `@ * param\nthis is the margin"); +verify.quickInfoIs("(parameter) y: string", "hi there `@ * param\nthis is the margin\n@param {string} z OOPS, unclosed backtick!"); +goTo.marker("z"); +verify.quickInfoIs("(parameter) z: any", undefined);