Skip to content
54 changes: 54 additions & 0 deletions internal/fourslash/tests/destructuredInterfaceJSDoc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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: /*4*/biz} = 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", "(property) FooBar.baz: string", "baz comment")
f.VerifyQuickInfoAt(t, "4", "const biz: 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")
}
45 changes: 43 additions & 2 deletions internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,37 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so familiar with this, so it could be right, but what I would say is that maybe this needs to be more general and look at all declarations nodes from a given symbol like getDocumentationComment did in the original codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f6b3006. Made the implementation more general by iterating through all symbol.Declarations (instead of just checking symbol.ValueDeclaration) to find binding elements. This matches the pattern in TypeScript where getDocumentationComment processes all declarations from a symbol.

}

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 (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
// Check all declarations to see if any is a binding element
if symbol != nil && ast.IsIdentifier(location) {
for _, decl := range symbol.Declarations {
if decl != nil && ast.IsBindingElement(decl) {
bindingElement := decl
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 {
propertySymbol := findPropertyInType(c, objectType, propertyName)
if propertySymbol != nil && propertySymbol.ValueDeclaration != nil {
declaration = propertySymbol.ValueDeclaration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I was actually expecting all comments for a given symbol to be smushed together, but that's not what the old codebase does either.

So I think we actually should switch back to the ValueDeclaration approach if we're doing anything like this.

Also, look for, or add, a test for quick info at 1 and 2.

interface Foo {
    /** This is bar from the interface */
    bar: string;
    /** This is baz from the interface */
    baz: number;
}

declare var foo: Foo;

/** Comment on the variable statement. */
const {
    /** Comment on bar destructuring. */ /*1*/bar,
    /** Comment on baz destructuring. */ /*2*/baz
} = foo;

@copilot go.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 73c9497. Switched back to the ValueDeclaration approach and restructured the logic to match TypeScript's behavior: first try to get JSDoc from the binding element itself, only fall back to the property's JSDoc if none is found.

Added test TestDestructuredWithOwnJSDoc that verifies the behavior when binding elements have JSDoc comments before them. The test shows that TypeScript doesn't currently attach JSDoc to individual binding elements in destructuring patterns, so both bar and baz correctly fall back to showing the interface property's JSDoc ("This is bar from the interface" and "This is baz from the interface").

break
}
}
}
}
}
}

if declaration == nil {
return ""
}
Expand Down Expand Up @@ -582,6 +609,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.IsUnion() {
for _, t := range objectType.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)
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/signaturehelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// | ```tsx
// | var foo: string
// | ```
// |
// | A description of foo
// | ----------------------------------------------------------------------
// }
[
Expand All @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
// | ```tsx
// | var a: { b: string; }
// | ```
// |
// | A description of 'a'
// | ----------------------------------------------------------------------
// b;
// ^
// | ----------------------------------------------------------------------
// | ```tsx
// | var b: string
// | ```
// |
// | A description of 'b'
// | ----------------------------------------------------------------------
// }
[
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// | ```tsx
// | var a: string | number
// | ```
// |
// | A description of a
// | ----------------------------------------------------------------------
// }
[
Expand All @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// | ```tsx
// | const isBar: boolean
// | ```
// |
// | Thing is a baz
// | ----------------------------------------------------------------------
[
{
Expand All @@ -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": {
Expand Down