From d53bbe949bb5dccded4564cbc00551924b60dfb7 Mon Sep 17 00:00:00 2001 From: Max Barinov Date: Tue, 3 Nov 2026 16:22:35 +0400 Subject: [PATCH 1/2] test(jsdoc): baseline inline links in optional type positions --- .../jsdocInlineLinksInTypePosition_test.go | 31 ++ ...ocInlineLinksInTypePosition.baseline.jsonc | 92 ++++++ .../jsdocInlineLinksInTypePosition.baseline | 276 ++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 tsc/internal/fourslash/tests/jsdocInlineLinksInTypePosition_test.go create mode 100644 tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc create mode 100644 tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline diff --git a/tsc/internal/fourslash/tests/jsdocInlineLinksInTypePosition_test.go b/tsc/internal/fourslash/tests/jsdocInlineLinksInTypePosition_test.go new file mode 100644 index 0000000000000..4606e65aae0b9 --- /dev/null +++ b/tsc/internal/fourslash/tests/jsdocInlineLinksInTypePosition_test.go @@ -0,0 +1,31 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestJsdocInlineLinksInTypePosition(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `class C {} +/** + * @throws {@link /*throwsLink*/C} + * @returns {@linkcode /*returnsLink*/C} + * @param value {@linkplain /*paramLink*/C} + */ +function /*inlineLinks*/f(value: C) {} + +/** + * @throws {/*throwsType*/C} description + * @returns {/*returnsType*/C} description + * @param {/*paramType*/C} value description + */ +function /*types*/g(value: C) { return value; }` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyBaselineHover(t) + f.VerifyBaselineGoToDefinition(t, false, "throwsLink", "returnsLink", "paramLink", "throwsType", "returnsType", "paramType") +} diff --git a/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc b/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc new file mode 100644 index 0000000000000..956f0b9dc9f35 --- /dev/null +++ b/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc @@ -0,0 +1,92 @@ +// === goToDefinition === +// === /jsdocInlineLinksInTypePosition.ts === +// class C {} +// /** +// * @throws {@link /*GOTO DEF*/C} +// * @returns {@linkcode C} +// * @param value {@linkplain C} +// */ +// --- (line: 7) skipped --- + + + +// === goToDefinition === +// === /jsdocInlineLinksInTypePosition.ts === +// class C {} +// /** +// * @throws {@link C} +// * @returns {@linkcode /*GOTO DEF*/C} +// * @param value {@linkplain C} +// */ +// function f(value: C) {} +// --- (line: 8) skipped --- + + + +// === goToDefinition === +// === /jsdocInlineLinksInTypePosition.ts === +// <|class [|C|] {}|> +// /** +// * @throws {@link C} +// * @returns {@linkcode C} +// * @param value {@linkplain /*GOTO DEF*/C} +// */ +// function f(value: C) {} +// +// --- (line: 9) skipped --- + + + +// === goToDefinition === +// === /jsdocInlineLinksInTypePosition.ts === +// <|class [|C|] {}|> +// /** +// * @throws {@link C} +// * @returns {@linkcode C} +// * @param value {@linkplain C} +// */ +// function f(value: C) {} +// +// /** +// * @throws {/*GOTO DEF*/C} description +// * @returns {C} description +// * @param {C} value description +// */ +// function g(value: C) { return value; } + + + +// === goToDefinition === +// === /jsdocInlineLinksInTypePosition.ts === +// <|class [|C|] {}|> +// /** +// * @throws {@link C} +// * @returns {@linkcode C} +// * @param value {@linkplain C} +// */ +// function f(value: C) {} +// +// /** +// * @throws {C} description +// * @returns {/*GOTO DEF*/C} description +// * @param {C} value description +// */ +// function g(value: C) { return value; } + + + +// === goToDefinition === +// === /jsdocInlineLinksInTypePosition.ts === +// <|class [|C|] {}|> +// /** +// * @throws {@link C} +// * @returns {@linkcode C} +// --- (line: 5) skipped --- + +// --- (line: 8) skipped --- +// /** +// * @throws {C} description +// * @returns {C} description +// * @param {/*GOTO DEF*/C} value description +// */ +// function g(value: C) { return value; } \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline b/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline new file mode 100644 index 0000000000000..5d4a7250f1405 --- /dev/null +++ b/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline @@ -0,0 +1,276 @@ +// === QuickInfo === +=== /jsdocInlineLinksInTypePosition.ts === +// class C {} +// /** +// * @throws {@link C} +// ^ +// | ---------------------------------------------------------------------- +// | No quickinfo at /*throwsLink*/. +// | ---------------------------------------------------------------------- +// * @returns {@linkcode C} +// ^ +// | ---------------------------------------------------------------------- +// | No quickinfo at /*returnsLink*/. +// | ---------------------------------------------------------------------- +// * @param value {@linkplain C} +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// */ +// function f(value: C) {} +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | function f(value: C): void +// | ``` +// | +// | +// | *@throws* — { +// | +// | *@link* — C} +// | +// | *@returns* +// | +// | *@linkcode* — C} +// | +// | *@param* `value` — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8) +// | ---------------------------------------------------------------------- +// +// /** +// * @throws {C} description +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// * @returns {C} description +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// * @param {C} value description +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// */ +// function g(value: C) { return value; } +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | function g(value: C): C +// | ``` +// | +// | +// | *@throws* — {C} description +// | +// | *@returns* — description +// | +// | *@param* `value` — description +// | ---------------------------------------------------------------------- +[ + { + "marker": { + "Position": 33, + "LSPosition": { + "line": 2, + "character": 18 + }, + "Name": "throwsLink", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 59, + "LSPosition": { + "line": 3, + "character": 23 + }, + "Name": "returnsLink", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 90, + "LSPosition": { + "line": 4, + "character": 28 + }, + "Name": "paramLink", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 4, + "character": 28 + }, + "end": { + "line": 4, + "character": 29 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "marker": { + "Position": 106, + "LSPosition": { + "line": 6, + "character": 9 + }, + "Name": "inlineLinks", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nfunction f(value: C): void\n```\n\n\n*@throws* — {\n\n*@link* — C}\n\n*@returns*\n\n*@linkcode* — C}\n\n*@param* `value` — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8)" + }, + "range": { + "start": { + "line": 6, + "character": 9 + }, + "end": { + "line": 6, + "character": 10 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "marker": { + "Position": 138, + "LSPosition": { + "line": 9, + "character": 12 + }, + "Name": "throwsType", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 9, + "character": 12 + }, + "end": { + "line": 9, + "character": 13 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "marker": { + "Position": 166, + "LSPosition": { + "line": 10, + "character": 13 + }, + "Name": "returnsType", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 10, + "character": 13 + }, + "end": { + "line": 10, + "character": 14 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "marker": { + "Position": 192, + "LSPosition": { + "line": 11, + "character": 11 + }, + "Name": "paramType", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 11, + "character": 11 + }, + "end": { + "line": 11, + "character": 12 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "marker": { + "Position": 226, + "LSPosition": { + "line": 13, + "character": 9 + }, + "Name": "types", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nfunction g(value: C): C\n```\n\n\n*@throws* — {C} description\n\n*@returns* — description\n\n*@param* `value` — description" + }, + "range": { + "start": { + "line": 13, + "character": 9 + }, + "end": { + "line": 13, + "character": 10 + } + }, + "canIncreaseVerbosity": true + } + } +] \ No newline at end of file From b671abaf1cbbf82208100ebe787c59ccd1e6a450 Mon Sep 17 00:00:00 2001 From: Max Barinov Date: Tue, 3 Nov 2026 16:34:16 +0400 Subject: [PATCH 2/2] fix(parser): preserve inline JSDoc links in optional type positions --- tsc/internal/parser/jsdoc.go | 5 +- tsc/internal/parser/parser_test.go | 52 +++++++++++++++++ ...ocInlineLinksInTypePosition.baseline.jsonc | 4 +- .../jsdocInlineLinksInTypePosition.baseline | 56 +++++++++++++++---- 4 files changed, 102 insertions(+), 15 deletions(-) diff --git a/tsc/internal/parser/jsdoc.go b/tsc/internal/parser/jsdoc.go index 6d239d2f228fc..e3ed0c8104dd4 100644 --- a/tsc/internal/parser/jsdoc.go +++ b/tsc/internal/parser/jsdoc.go @@ -783,7 +783,8 @@ func (p *Parser) parseUnknownTag(start int, tagName *ast.IdentifierNode, indent func (p *Parser) tryParseTypeExpression() *ast.Node { p.skipWhitespaceOrAsterisk() - if p.token == ast.KindOpenBraceToken { + // An inline link starts the tag's description, not a type annotation. + if p.token == ast.KindOpenBraceToken && p.lookAhead(func(p *Parser) bool { _, ok := p.parseJSDocLinkPrefix(); return !ok }) { return p.parseJSDocTypeExpression(false /*mayOmitBraces*/) } else { return nil @@ -838,7 +839,7 @@ func (p *Parser) parseParameterOrPropertyTag(start int, tagName *ast.IdentifierN name, isBracketed := p.parseBracketNameInPropertyAndParamTag(target) indentText := p.skipWhitespaceOrAsterisk() - if isNameFirst && p.lookAhead(func(p *Parser) bool { _, ok := p.parseJSDocLinkPrefix(); return !ok }) { + if isNameFirst { typeExpression = p.tryParseTypeExpression() } diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index ffa96713eb444..5d63241fc67dc 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -365,6 +365,58 @@ func TestParseEscapedDynamicImportPhase(t *testing.T) { } } +func TestJSDocInlineLinksInOptionalTypePositions(t *testing.T) { + t.Parallel() + for _, tag := range []string{"throws", "returns", "param value"} { + for _, link := range []struct { + name string + kind ast.Kind + }{ + {"link", ast.KindJSDocLink}, + {"linkcode", ast.KindJSDocLinkCode}, + {"linkplain", ast.KindJSDocLinkPlain}, + } { + t.Run(tag+"/"+link.name, func(t *testing.T) { + t.Parallel() + file := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/index.ts", + PathKey: "/index.ts", + }, "/** @"+tag+" {@"+link.name+" C} */\nfunction f(value) {}", core.ScriptKindTS) + jsDocs := file.Statements.Nodes[0].JSDoc(file) + assert.Equal(t, len(jsDocs), 1) + tags := jsDocs[0].AsJSDoc().Tags.Nodes + assert.Equal(t, len(tags), 1) + assert.Assert(t, tags[0].TypeExpression() == nil) + comments := tags[0].CommentList() + assert.Assert(t, comments != nil) + links := core.Filter(comments.Nodes, ast.IsJSDocLinkLike) + assert.Equal(t, len(links), 1) + assert.Equal(t, links[0].Kind, link.kind) + }) + } + } +} + +func TestJSDocTypesInOptionalTypePositions(t *testing.T) { + t.Parallel() + for _, tag := range []string{"throws", "returns", "param value"} { + t.Run(tag, func(t *testing.T) { + t.Parallel() + file := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/index.ts", + PathKey: "/index.ts", + }, "/** @"+tag+" {C} description */\nfunction f(value) {}", core.ScriptKindTS) + jsDocs := file.Statements.Nodes[0].JSDoc(file) + assert.Equal(t, len(jsDocs), 1) + tags := jsDocs[0].AsJSDoc().Tags.Nodes + assert.Equal(t, len(tags), 1) + typeExpression := tags[0].TypeExpression() + assert.Assert(t, typeExpression != nil) + assert.Equal(t, scanner.GetTextOfNode(typeExpression.Type()), "C") + }) + } +} + func TestJSDocImportTypeParentChain(t *testing.T) { t.Parallel() sourceText := `test("", async function () { diff --git a/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc b/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc index 956f0b9dc9f35..cc8202db0955c 100644 --- a/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc +++ b/tsc/testdata/baselines/reference/fourslash/goToDefinition/jsdocInlineLinksInTypePosition.baseline.jsonc @@ -1,6 +1,6 @@ // === goToDefinition === // === /jsdocInlineLinksInTypePosition.ts === -// class C {} +// <|class [|C|] {}|> // /** // * @throws {@link /*GOTO DEF*/C} // * @returns {@linkcode C} @@ -12,7 +12,7 @@ // === goToDefinition === // === /jsdocInlineLinksInTypePosition.ts === -// class C {} +// <|class [|C|] {}|> // /** // * @throws {@link C} // * @returns {@linkcode /*GOTO DEF*/C} diff --git a/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline b/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline index 5d4a7250f1405..7820505d57ef3 100644 --- a/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline +++ b/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline @@ -5,12 +5,18 @@ // * @throws {@link C} // ^ // | ---------------------------------------------------------------------- -// | No quickinfo at /*throwsLink*/. +// | ```typescript +// | class C +// | ``` +// | // | ---------------------------------------------------------------------- // * @returns {@linkcode C} // ^ // | ---------------------------------------------------------------------- -// | No quickinfo at /*returnsLink*/. +// | ```typescript +// | class C +// | ``` +// | // | ---------------------------------------------------------------------- // * @param value {@linkplain C} // ^ @@ -29,13 +35,9 @@ // | ``` // | // | -// | *@throws* — { -// | -// | *@link* — C} +// | *@throws* — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8) // | -// | *@returns* -// | -// | *@linkcode* — C} +// | *@returns* — [`C`](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8) // | // | *@param* `value` — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8) // | ---------------------------------------------------------------------- @@ -91,7 +93,23 @@ "Name": "throwsLink", "Data": {} }, - "item": null + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 2, + "character": 18 + }, + "end": { + "line": 2, + "character": 19 + } + }, + "canIncreaseVerbosity": true + } }, { "marker": { @@ -103,7 +121,23 @@ "Name": "returnsLink", "Data": {} }, - "item": null + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 3, + "character": 23 + }, + "end": { + "line": 3, + "character": 24 + } + }, + "canIncreaseVerbosity": true + } }, { "marker": { @@ -146,7 +180,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```typescript\nfunction f(value: C): void\n```\n\n\n*@throws* — {\n\n*@link* — C}\n\n*@returns*\n\n*@linkcode* — C}\n\n*@param* `value` — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8)" + "value": "```typescript\nfunction f(value: C): void\n```\n\n\n*@throws* — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8)\n\n*@returns* — [`C`](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8)\n\n*@param* `value` — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8)" }, "range": { "start": {