Skip to content
81 changes: 81 additions & 0 deletions internal/fourslash/tests/destructuredInterfaceJSDoc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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")
}

func TestDestructuredWithOwnJSDoc(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `
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;
`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
// When binding elements have their own JSDoc, TypeScript doesn't currently show it in hover
// Instead, it falls back to the property's JSDoc from the interface
f.VerifyQuickInfoAt(t, "1", "const bar: string", "This is bar from the interface")
f.VerifyQuickInfoAt(t, "2", "const baz: number", "This is baz from the interface")
}
44 changes: 41 additions & 3 deletions internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,40 @@ 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 {
if declaration == nil {
return ""
}

isMarkdown := contentFormat == lsproto.MarkupKindMarkdown
var b strings.Builder
if jsdoc := getJSDocOrTag(c, declaration); jsdoc != nil && !containsTypedefTag(jsdoc) {
jsdoc := getJSDocOrTag(c, declaration)

// Handle binding elements specially (variables created from destructuring) - we need to get the documentation from the property type
// If the binding element doesn't have its own JSDoc, fall back to the property's JSDoc
if jsdoc == nil && 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 {
propertySymbol := findPropertyInType(c, objectType, propertyName)
if propertySymbol != nil && propertySymbol.ValueDeclaration != nil {
jsdoc = getJSDocOrTag(c, propertySymbol.ValueDeclaration)
}
}
}
}

if jsdoc != nil && !containsTypedefTag(jsdoc) {
l.writeComments(&b, c, jsdoc.Comments(), isMarkdown)
if jsdoc.Kind == ast.KindJSDoc {
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
Expand Down Expand Up @@ -582,6 +606,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