From de2f3336bf89fcdf85f02fe96d34ca04760629e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:42:54 +0000 Subject: [PATCH 1/6] Initial plan From bfff6ef1341db41c1a733d678dd1c9f9248612b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:56:44 +0000 Subject: [PATCH 2/6] Add test for destructured interface JSDoc comments Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- .../tests/destructuredInterfaceJSDoc_test.go | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 internal/fourslash/tests/destructuredInterfaceJSDoc_test.go diff --git a/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go b/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go new file mode 100644 index 0000000000..5f396a6c83 --- /dev/null +++ b/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go @@ -0,0 +1,32 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestDestructuredInterfaceJSDoc(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +interface FooBar { + /** foo comment */ + foo: number; + /** bar comment */ + bar: string; + /** baz comment */ + baz: string; +} + +declare const fubar: FooBar; + +const {/*1*/foo, /*2*/bar, /*3*/baz} = fubar; +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyQuickInfoAt(t, "1", "const foo: number", "foo comment") + f.VerifyQuickInfoAt(t, "2", "const bar: string", "bar comment") + f.VerifyQuickInfoAt(t, "3", "const baz: string", "baz comment") +} From 2462931f3d254091096b0406a6b4372291b3c722 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:03:20 +0000 Subject: [PATCH 3/6] Fix destructured interface members to show JSDoc comments Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- .../tests/destructuredInterfaceJSDoc_test.go | 21 +++++++++++ internal/ls/hover.go | 37 ++++++++++++++++++- internal/ls/signaturehelp.go | 2 +- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go b/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go index 5f396a6c83..8981a1a6db 100644 --- a/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go +++ b/internal/fourslash/tests/destructuredInterfaceJSDoc_test.go @@ -30,3 +30,24 @@ const {/*1*/foo, /*2*/bar, /*3*/baz} = fubar; f.VerifyQuickInfoAt(t, "2", "const bar: string", "bar comment") f.VerifyQuickInfoAt(t, "3", "const baz: string", "baz comment") } + +func TestDestructuredInterfaceJSDocWithRename(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +interface FooBar { + /** foo comment */ + foo: number; + /** bar comment */ + bar: string; +} + +declare const fubar: FooBar; + +const {foo: /*1*/myFoo, bar: /*2*/myBar} = fubar; +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyQuickInfoAt(t, "1", "const myFoo: number", "foo comment") + f.VerifyQuickInfoAt(t, "2", "const myBar: string", "bar comment") +} diff --git a/internal/ls/hover.go b/internal/ls/hover.go index 6c8c644adc..bd56362bcb 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -67,10 +67,43 @@ func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Check if quickInfo == "" { return "", "" } - return quickInfo, l.getDocumentationFromDeclaration(c, declaration, contentFormat) + return quickInfo, l.getDocumentationFromDeclaration(c, symbol, declaration, node, contentFormat) } -func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, declaration *ast.Node, contentFormat lsproto.MarkupKind) string { +func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, symbol *ast.Symbol, declaration *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind) string { + // Handle binding elements specially - we need to get the documentation from the property type + // The declaration passed in might be the binding element itself, but we need the interface property declaration + if symbol != nil && symbol.ValueDeclaration != nil && ast.IsBindingElement(symbol.ValueDeclaration) && ast.IsIdentifier(location) { + bindingElement := symbol.ValueDeclaration + parent := bindingElement.Parent + name := bindingElement.PropertyName() + if name == nil { + name = bindingElement.Name() + } + if ast.IsIdentifier(name) && ast.IsObjectBindingPattern(parent) { + propertyName := name.Text() + objectType := c.GetTypeAtLocation(parent) + if objectType != nil { + // For union types, try to find the property in any of the constituent types + var propertySymbol *ast.Symbol + if objectType.Flags()&checker.TypeFlagsUnion != 0 { + for _, t := range objectType.AsUnionType().Types() { + prop := c.GetPropertyOfType(t, propertyName) + if prop != nil { + propertySymbol = prop + break + } + } + } else { + propertySymbol = c.GetPropertyOfType(objectType, propertyName) + } + if propertySymbol != nil && propertySymbol.ValueDeclaration != nil { + declaration = propertySymbol.ValueDeclaration + } + } + } + } + if declaration == nil { return "" } diff --git a/internal/ls/signaturehelp.go b/internal/ls/signaturehelp.go index c404a764bd..68bc385e6d 100644 --- a/internal/ls/signaturehelp.go +++ b/internal/ls/signaturehelp.go @@ -428,7 +428,7 @@ func (l *LanguageService) getSignatureHelpItem(candidate *checker.Signature, isT // Generate documentation from the signature's declaration var documentation *string if declaration := candidate.Declaration(); declaration != nil { - doc := l.getDocumentationFromDeclaration(c, declaration, docFormat) + doc := l.getDocumentationFromDeclaration(c, nil, declaration, nil, docFormat) if doc != "" { documentation = &doc } From daf041ff30df7b6bce618ce54677f64255c8a0cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:09:16 +0000 Subject: [PATCH 4/6] Update baselines for destructured binding element JSDoc Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- .../quickInfoForObjectBindingElementName03.baseline | 4 ++-- .../quickInfoForObjectBindingElementName04.baseline | 8 ++++---- .../quickInfoForObjectBindingElementName05.baseline | 4 ++-- .../quickInfoForObjectBindingElementName06.baseline | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName03.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName03.baseline index 0e0a6ea1c5..05542c9250 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName03.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName03.baseline @@ -14,7 +14,7 @@ // | ```tsx // | var foo: string // | ``` -// | +// | A description of foo // | ---------------------------------------------------------------------- // } [ @@ -31,7 +31,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\nvar foo: string\n```\n" + "value": "```tsx\nvar foo: string\n```\nA description of foo" }, "range": { "start": { diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName04.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName04.baseline index 926bfdf720..049f680029 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName04.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName04.baseline @@ -19,7 +19,7 @@ // | ```tsx // | var a: { b: string; } // | ``` -// | +// | A description of 'a' // | ---------------------------------------------------------------------- // b; // ^ @@ -27,7 +27,7 @@ // | ```tsx // | var b: string // | ``` -// | +// | A description of 'b' // | ---------------------------------------------------------------------- // } [ @@ -44,7 +44,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\nvar a: { b: string; }\n```\n" + "value": "```tsx\nvar a: { b: string; }\n```\nA description of 'a'" }, "range": { "start": { @@ -71,7 +71,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\nvar b: string\n```\n" + "value": "```tsx\nvar b: string\n```\nA description of 'b'" }, "range": { "start": { diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName05.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName05.baseline index 8ba464b2a4..68f0adb544 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName05.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName05.baseline @@ -17,7 +17,7 @@ // | ```tsx // | var a: string | number // | ``` -// | +// | A description of a // | ---------------------------------------------------------------------- // } [ @@ -34,7 +34,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\nvar a: string | number\n```\n" + "value": "```tsx\nvar a: string | number\n```\nA description of a" }, "range": { "start": { diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName06.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName06.baseline index 71b808900c..a0431ca1be 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName06.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoForObjectBindingElementName06.baseline @@ -23,7 +23,7 @@ // | ```tsx // | const isBar: boolean // | ``` -// | +// | Thing is a baz // | ---------------------------------------------------------------------- [ { @@ -39,7 +39,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\nconst isBar: boolean\n```\n" + "value": "```tsx\nconst isBar: boolean\n```\nThing is a baz" }, "range": { "start": { From ff34e995ab61b18eeef8f7e74e14bf48e134d03d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:14:38 +0000 Subject: [PATCH 5/6] Refactor: Extract findPropertyInType helper and improve comments Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/ls/hover.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/internal/ls/hover.go b/internal/ls/hover.go index bd56362bcb..20779f4e46 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -71,7 +71,7 @@ func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Check } func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, symbol *ast.Symbol, declaration *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind) string { - // Handle binding elements specially - we need to get the documentation from the property type + // Handle binding elements specially (variables created from destructuring) - we need to get the documentation from the property type // The declaration passed in might be the binding element itself, but we need the interface property declaration if symbol != nil && symbol.ValueDeclaration != nil && ast.IsBindingElement(symbol.ValueDeclaration) && ast.IsIdentifier(location) { bindingElement := symbol.ValueDeclaration @@ -84,19 +84,7 @@ func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, sy propertyName := name.Text() objectType := c.GetTypeAtLocation(parent) if objectType != nil { - // For union types, try to find the property in any of the constituent types - var propertySymbol *ast.Symbol - if objectType.Flags()&checker.TypeFlagsUnion != 0 { - for _, t := range objectType.AsUnionType().Types() { - prop := c.GetPropertyOfType(t, propertyName) - if prop != nil { - propertySymbol = prop - break - } - } - } else { - propertySymbol = c.GetPropertyOfType(objectType, propertyName) - } + propertySymbol := findPropertyInType(c, objectType, propertyName) if propertySymbol != nil && propertySymbol.ValueDeclaration != nil { declaration = propertySymbol.ValueDeclaration } @@ -615,6 +603,20 @@ func writeQuotedString(b *strings.Builder, str string, quote bool) { } } +// findPropertyInType finds a property in a type, handling union types by searching constituent types +func findPropertyInType(c *checker.Checker, objectType *checker.Type, propertyName string) *ast.Symbol { + // For union types, try to find the property in any of the constituent types + if objectType.Flags()&checker.TypeFlagsUnion != 0 { + for _, t := range objectType.AsUnionType().Types() { + if prop := c.GetPropertyOfType(t, propertyName); prop != nil { + return prop + } + } + return nil + } + return c.GetPropertyOfType(objectType, propertyName) +} + func getEntityNameString(name *ast.Node) string { var b strings.Builder writeEntityNameParts(&b, name) From f0f9fef1c9c6040d2bdaa92d4b7bcddb0ceca8a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:08:26 +0000 Subject: [PATCH 6/6] Simplify findPropertyInType using IsUnion() and Types() methods Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/ls/hover.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ls/hover.go b/internal/ls/hover.go index 20779f4e46..172269c231 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -606,8 +606,8 @@ func writeQuotedString(b *strings.Builder, str string, quote bool) { // findPropertyInType finds a property in a type, handling union types by searching constituent types func findPropertyInType(c *checker.Checker, objectType *checker.Type, propertyName string) *ast.Symbol { // For union types, try to find the property in any of the constituent types - if objectType.Flags()&checker.TypeFlagsUnion != 0 { - for _, t := range objectType.AsUnionType().Types() { + if objectType.IsUnion() { + for _, t := range objectType.Types() { if prop := c.GetPropertyOfType(t, propertyName); prop != nil { return prop }