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 dbcffe1680e..7da0f03008a 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 00000000000..6d78551a307 --- /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 83bd81113aa..d25a7ebbb1c 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 8127e836df4..42f10e8ea5b 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 942ee98d52f..0a0c1d5406a 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 16ddce5a939..9610e1895e6 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);