Skip to content

Commit c3a9429

Browse files
authored
Handle JSDoc backticks in the parser, not scanner (#30939)
* Scan backticks in jsdoc as a single token less often Previously, matching backticks in jsdoc would always be scanned as one token to aid parsing incorrect jsdoc that uses backticks for parameter names: ``js /** @param {string} `nope` * @param {number} `not needed` */ ``` However, this is wrong for code fences, which use triple backticks. This fix parses a single backtick as a single token if it's immediately followed by another backtick or by a newline. It retains the questionable tokenisation of backticks-as-pairs in other cases, however. A better fix might be to have the parser ignore backticks in jsdoc instead. * Add test case * Handle jsdoc backticks in the parser, not scanner Previously, the jsdoc scanner had ad-hoc handling of backticks that was similar in structure to the normal scanner's handling, but much simpler. This was a smaller code change, but is handled backwards: the special case of backtick-quoted parameter names was handled in the scanner instead of in the jsdoc identifier parsing code. That made it overapply and block correct handling of asterisks across newlines, which was most obvious in code fences inside jsdoc, as in #23517. Fixes #23517 * More cleanup
1 parent 0574c1f commit c3a9429

9 files changed

+772
-606
lines changed

src/compiler/parser.ts

+59-27
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,10 @@ namespace ts {
10811081
return currentToken = scanner.scan();
10821082
}
10831083

1084+
function nextTokenJSDoc(): JSDocSyntaxKind {
1085+
return currentToken = scanner.scanJsDocToken();
1086+
}
1087+
10841088
function reScanGreaterToken(): SyntaxKind {
10851089
return currentToken = scanner.reScanGreaterToken();
10861090
}
@@ -1198,6 +1202,15 @@ namespace ts {
11981202
return false;
11991203
}
12001204

1205+
function parseExpectedJSDoc(kind: JSDocSyntaxKind) {
1206+
if (token() === kind) {
1207+
nextTokenJSDoc();
1208+
return true;
1209+
}
1210+
parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(kind));
1211+
return false;
1212+
}
1213+
12011214
function parseOptional(t: SyntaxKind): boolean {
12021215
if (token() === t) {
12031216
nextToken();
@@ -1214,18 +1227,38 @@ namespace ts {
12141227
return undefined;
12151228
}
12161229

1230+
function parseOptionalTokenJSDoc<TKind extends JSDocSyntaxKind>(t: TKind): Token<TKind>;
1231+
function parseOptionalTokenJSDoc(t: JSDocSyntaxKind): Node | undefined {
1232+
if (token() === t) {
1233+
return parseTokenNodeJSDoc();
1234+
}
1235+
return undefined;
1236+
}
1237+
12171238
function parseExpectedToken<TKind extends SyntaxKind>(t: TKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Token<TKind>;
12181239
function parseExpectedToken(t: SyntaxKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Node {
12191240
return parseOptionalToken(t) ||
12201241
createMissingNode(t, /*reportAtCurrentPosition*/ false, diagnosticMessage || Diagnostics._0_expected, arg0 || tokenToString(t));
12211242
}
12221243

1244+
function parseExpectedTokenJSDoc<TKind extends JSDocSyntaxKind>(t: TKind): Token<TKind>;
1245+
function parseExpectedTokenJSDoc(t: JSDocSyntaxKind): Node {
1246+
return parseOptionalTokenJSDoc(t) ||
1247+
createMissingNode(t, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(t));
1248+
}
1249+
12231250
function parseTokenNode<T extends Node>(): T {
12241251
const node = <T>createNode(token());
12251252
nextToken();
12261253
return finishNode(node);
12271254
}
12281255

1256+
function parseTokenNodeJSDoc<T extends Node>(): T {
1257+
const node = <T>createNode(token());
1258+
nextTokenJSDoc();
1259+
return finishNode(node);
1260+
}
1261+
12291262
function canParseSemicolon() {
12301263
// If there's a real semicolon, then we can always parse it out.
12311264
if (token() === SyntaxKind.SemicolonToken) {
@@ -6345,7 +6378,7 @@ namespace ts {
63456378
const hasBrace = (mayOmitBraces ? parseOptional : parseExpected)(SyntaxKind.OpenBraceToken);
63466379
result.type = doInsideOfContext(NodeFlags.JSDoc, parseJSDocType);
63476380
if (!mayOmitBraces || hasBrace) {
6348-
parseExpected(SyntaxKind.CloseBraceToken);
6381+
parseExpectedJSDoc(SyntaxKind.CloseBraceToken);
63496382
}
63506383

63516384
fixupParentReferences(result);
@@ -6432,7 +6465,7 @@ namespace ts {
64326465
indent += text.length;
64336466
}
64346467

6435-
nextJSDocToken();
6468+
nextTokenJSDoc();
64366469
while (parseOptionalJsdoc(SyntaxKind.WhitespaceTrivia));
64376470
if (parseOptionalJsdoc(SyntaxKind.NewLineTrivia)) {
64386471
state = JSDocState.BeginningOfLine;
@@ -6493,7 +6526,7 @@ namespace ts {
64936526
pushComment(scanner.getTokenText());
64946527
break;
64956528
}
6496-
nextJSDocToken();
6529+
nextTokenJSDoc();
64976530
}
64986531
removeLeadingNewlines(comments);
64996532
removeTrailingWhitespace(comments);
@@ -6522,7 +6555,7 @@ namespace ts {
65226555
function isNextNonwhitespaceTokenEndOfFile(): boolean {
65236556
// We must use infinite lookahead, as there could be any number of newlines :(
65246557
while (true) {
6525-
nextJSDocToken();
6558+
nextTokenJSDoc();
65266559
if (token() === SyntaxKind.EndOfFileToken) {
65276560
return true;
65286561
}
@@ -6539,7 +6572,7 @@ namespace ts {
65396572
}
65406573
}
65416574
while (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) {
6542-
nextJSDocToken();
6575+
nextTokenJSDoc();
65436576
}
65446577
}
65456578

@@ -6563,15 +6596,15 @@ namespace ts {
65636596
else if (token() === SyntaxKind.AsteriskToken) {
65646597
precedingLineBreak = false;
65656598
}
6566-
nextJSDocToken();
6599+
nextTokenJSDoc();
65676600
}
65686601
return seenLineBreak ? indentText : "";
65696602
}
65706603

65716604
function parseTag(margin: number) {
65726605
Debug.assert(token() === SyntaxKind.AtToken);
65736606
const start = scanner.getTokenPos();
6574-
nextJSDocToken();
6607+
nextTokenJSDoc();
65756608

65766609
const tagName = parseJSDocIdentifierName(/*message*/ undefined);
65776610
const indentText = skipWhitespaceOrAsterisk();
@@ -6643,7 +6676,7 @@ namespace ts {
66436676
pushComment(initialMargin);
66446677
state = JSDocState.SavingComments;
66456678
}
6646-
let tok = token() as JsDocSyntaxKind;
6679+
let tok = token() as JSDocSyntaxKind;
66476680
loop: while (true) {
66486681
switch (tok) {
66496682
case SyntaxKind.NewLineTrivia:
@@ -6674,11 +6707,11 @@ namespace ts {
66746707
break;
66756708
case SyntaxKind.OpenBraceToken:
66766709
state = JSDocState.SavingComments;
6677-
if (lookAhead(() => nextJSDocToken() === SyntaxKind.AtToken && tokenIsIdentifierOrKeyword(nextJSDocToken()) && scanner.getTokenText() === "link")) {
6710+
if (lookAhead(() => nextTokenJSDoc() === SyntaxKind.AtToken && tokenIsIdentifierOrKeyword(nextTokenJSDoc()) && scanner.getTokenText() === "link")) {
66786711
pushComment(scanner.getTokenText());
6679-
nextJSDocToken();
6712+
nextTokenJSDoc();
66806713
pushComment(scanner.getTokenText());
6681-
nextJSDocToken();
6714+
nextTokenJSDoc();
66826715
}
66836716
pushComment(scanner.getTokenText());
66846717
break;
@@ -6696,7 +6729,7 @@ namespace ts {
66966729
pushComment(scanner.getTokenText());
66976730
break;
66986731
}
6699-
tok = nextJSDocToken();
6732+
tok = nextTokenJSDoc();
67006733
}
67016734

67026735
removeLeadingNewlines(comments);
@@ -6730,16 +6763,19 @@ namespace ts {
67306763
}
67316764

67326765
function parseBracketNameInPropertyAndParamTag(): { name: EntityName, isBracketed: boolean } {
6733-
if (token() === SyntaxKind.NoSubstitutionTemplateLiteral) {
6734-
// a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild
6735-
return { name: createIdentifier(/*isIdentifier*/ true), isBracketed: false };
6736-
}
67376766
// Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar'
6738-
const isBracketed = parseOptional(SyntaxKind.OpenBracketToken);
6767+
const isBracketed = parseOptionalJsdoc(SyntaxKind.OpenBracketToken);
6768+
if (isBracketed) {
6769+
skipWhitespace();
6770+
}
6771+
// a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild
6772+
const isBackquoted = parseOptionalJsdoc(SyntaxKind.BacktickToken);
67396773
const name = parseJSDocEntityName();
6774+
if (isBackquoted) {
6775+
parseExpectedTokenJSDoc(SyntaxKind.BacktickToken);
6776+
}
67406777
if (isBracketed) {
67416778
skipWhitespace();
6742-
67436779
// May have an optional default, e.g. '[foo = 42]'
67446780
if (parseOptionalToken(SyntaxKind.EqualsToken)) {
67456781
parseExpression();
@@ -7022,7 +7058,7 @@ namespace ts {
70227058
let canParseTag = true;
70237059
let seenAsterisk = false;
70247060
while (true) {
7025-
switch (nextJSDocToken()) {
7061+
switch (nextTokenJSDoc()) {
70267062
case SyntaxKind.AtToken:
70277063
if (canParseTag) {
70287064
const child = tryParseChildTag(target, indent);
@@ -7057,7 +7093,7 @@ namespace ts {
70577093
function tryParseChildTag(target: PropertyLikeParse, indent: number): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false {
70587094
Debug.assert(token() === SyntaxKind.AtToken);
70597095
const start = scanner.getStartPos();
7060-
nextJSDocToken();
7096+
nextTokenJSDoc();
70617097

70627098
const tagName = parseJSDocIdentifierName();
70637099
skipWhitespace();
@@ -7109,13 +7145,9 @@ namespace ts {
71097145
return result;
71107146
}
71117147

7112-
function nextJSDocToken(): JsDocSyntaxKind {
7113-
return currentToken = scanner.scanJSDocToken();
7114-
}
7115-
7116-
function parseOptionalJsdoc(t: JsDocSyntaxKind): boolean {
7148+
function parseOptionalJsdoc(t: JSDocSyntaxKind): boolean {
71177149
if (token() === t) {
7118-
nextJSDocToken();
7150+
nextTokenJSDoc();
71197151
return true;
71207152
}
71217153
return false;
@@ -7150,7 +7182,7 @@ namespace ts {
71507182
result.escapedText = escapeLeadingUnderscores(scanner.getTokenText());
71517183
finishNode(result, end);
71527184

7153-
nextJSDocToken();
7185+
nextTokenJSDoc();
71547186
return result;
71557187
}
71567188
}

src/compiler/scanner.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ namespace ts {
3333
reScanJsxToken(): JsxTokenSyntaxKind;
3434
reScanLessThanToken(): SyntaxKind;
3535
scanJsxToken(): JsxTokenSyntaxKind;
36-
scanJSDocToken(): JsDocSyntaxKind;
36+
scanJsDocToken(): JSDocSyntaxKind;
3737
scan(): SyntaxKind;
3838
getText(): string;
3939
// Sets the text for the scanner to scan. An optional subrange starting point and length
@@ -886,7 +886,7 @@ namespace ts {
886886
reScanJsxToken,
887887
reScanLessThanToken,
888888
scanJsxToken,
889-
scanJSDocToken,
889+
scanJsDocToken,
890890
scan,
891891
getText,
892892
setText,
@@ -2050,7 +2050,7 @@ namespace ts {
20502050
}
20512051
}
20522052

2053-
function scanJSDocToken(): JsDocSyntaxKind {
2053+
function scanJsDocToken(): JSDocSyntaxKind {
20542054
startPos = tokenPos = pos;
20552055
tokenFlags = 0;
20562056
if (pos >= end) {
@@ -2093,12 +2093,7 @@ namespace ts {
20932093
case CharacterCodes.dot:
20942094
return token = SyntaxKind.DotToken;
20952095
case CharacterCodes.backtick:
2096-
while (pos < end && text.charCodeAt(pos) !== CharacterCodes.backtick) {
2097-
pos++;
2098-
}
2099-
tokenValue = text.substring(tokenPos + 1, pos);
2100-
pos++;
2101-
return token = SyntaxKind.NoSubstitutionTemplateLiteral;
2096+
return token = SyntaxKind.BacktickToken;
21022097
}
21032098

21042099
if (isIdentifierStart(ch, ScriptTarget.Latest)) {

src/compiler/types.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace ts {
88
end: number;
99
}
1010

11-
export type JsDocSyntaxKind =
11+
export type JSDocSyntaxKind =
1212
| SyntaxKind.EndOfFileToken
1313
| SyntaxKind.WhitespaceTrivia
1414
| SyntaxKind.AtToken
@@ -23,7 +23,7 @@ namespace ts {
2323
| SyntaxKind.CommaToken
2424
| SyntaxKind.DotToken
2525
| SyntaxKind.Identifier
26-
| SyntaxKind.NoSubstitutionTemplateLiteral
26+
| SyntaxKind.BacktickToken
2727
| SyntaxKind.Unknown
2828
| KeywordSyntaxKind;
2929

@@ -181,6 +181,8 @@ namespace ts {
181181
QuestionToken,
182182
ColonToken,
183183
AtToken,
184+
/** Only the JSDoc scanner produces BacktickToken. The normal scanner produces NoSubstitutionTemplateLiteral and related kinds. */
185+
BacktickToken,
184186
// Assignments
185187
EqualsToken,
186188
PlusEqualsToken,

tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"0": {
99
"kind": "JSDocParameterTag",
1010
"pos": 8,
11-
"end": 29,
11+
"end": 32,
1212
"modifierFlagsCache": 0,
1313
"transformFlags": 0,
1414
"tagName": {
@@ -46,6 +46,6 @@
4646
},
4747
"length": 1,
4848
"pos": 8,
49-
"end": 29
49+
"end": 32
5050
}
5151
}

0 commit comments

Comments
 (0)