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/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 new file mode 100644 index 0000000000000..cc8202db0955c --- /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..7820505d57ef3 --- /dev/null +++ b/tsc/testdata/baselines/reference/fourslash/quickInfo/jsdocInlineLinksInTypePosition.baseline @@ -0,0 +1,310 @@ +// === QuickInfo === +=== /jsdocInlineLinksInTypePosition.ts === +// class C {} +// /** +// * @throws {@link C} +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// * @returns {@linkcode C} +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// * @param value {@linkplain C} +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | class C +// | ``` +// | +// | ---------------------------------------------------------------------- +// */ +// function f(value: C) {} +// ^ +// | ---------------------------------------------------------------------- +// | ```typescript +// | function f(value: C): void +// | ``` +// | +// | +// | *@throws* — [C](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8) +// | +// | *@returns* — [`C`](file:///jsdocInlineLinksInTypePosition.ts#1,7-1,8) +// | +// | *@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": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 2, + "character": 18 + }, + "end": { + "line": 2, + "character": 19 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "marker": { + "Position": 59, + "LSPosition": { + "line": 3, + "character": 23 + }, + "Name": "returnsLink", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```typescript\nclass C\n```\n" + }, + "range": { + "start": { + "line": 3, + "character": 23 + }, + "end": { + "line": 3, + "character": 24 + } + }, + "canIncreaseVerbosity": true + } + }, + { + "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* — [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": { + "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