Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestCompletionsForContextualConstraintTypeInJsDoc(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")

const content = `
// @allowJs: true
// @filename: a.ts
export interface Blah<T extends { a: "hello" | "world" }> {
}

// @filename: b.js
/** @import * as a from "./a" */

/** @type {a.Blah<{ a: /*1*/ }>} */
let x;

// @filename: c.js
/** @import * as a from "./a" */

/** @type {a.Blah<{ a: /*2*/ }>} */
`

f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()

// These examples both would panic in retrieving the symbols
// of property signature nodes within JSDoc types.
// In both cases, we'd have a JSDoc property signature that has no symbol.
//
// The two cases differ in whether or not there is a variable declaration
// following the `@type` comment. These are important to test differently
// because of how JSDoc re-parsing would construct nodes in the tree.
//
// Getting the symbol of the reparsed node is a sufficient fix for marker 1.
// However, that would not fix the case at marker 2 because
// there is no variable to attach the `@type` annotation, so the node basically
// doesn't exist for subsequent passes like the binder.

f.VerifyCompletions(t, f.Markers(), &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{CommitCharacters: &[]string{".", ",", ";"}},
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

CompletionsExpectedItemDefaults.EditRange is left as nil here, which will assert that the server returns a nil CompletionItemDefaults.EditRange. For type-literal completions the server typically sets an edit range; this test should set EditRange to Ignored (or assert the exact range) to avoid a hard failure.

Suggested change
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{CommitCharacters: &[]string{".", ",", ";"}},
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &[]string{".", ",", ";"},
EditRange: fourslash.Ignored,
},

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Hard failure? What?

I do wonder if we need to specify any of these ItemDefaults though.

Items: &fourslash.CompletionsExpectedItems{
Includes: []fourslash.CompletionsExpectedItem{
`"hello"`,
`"world"`,
},
},
})
}
15 changes: 14 additions & 1 deletion internal/ls/completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3655,7 +3655,20 @@ func getConstraintOfTypeArgumentProperty(node *ast.Node, typeChecker *checker.Ch

switch node.Kind {
case ast.KindPropertySignature:
return typeChecker.GetTypeOfPropertyOfContextualType(t, node.Symbol().Name)
// Try to get the reparsed node first - we may be in JSDoc.
reparsed := ast.GetReparsedNodeForNode(node)
if symbol := reparsed.Symbol(); symbol != nil {
return typeChecker.GetTypeOfPropertyOfContextualType(t, symbol.Name)
}

// In some cases, we won't have a corresponding symbol
// (e.g. JSDoc types that never get re-attached) so we'll use
// the name as declared by the property as a best-effort.
if name, ok := ast.TryGetTextOfPropertyName(reparsed.Name()); ok {
return typeChecker.GetTypeOfPropertyOfContextualType(t, name)
}

return nil
case ast.KindColonToken:
if node.Parent.Kind == ast.KindPropertySignature {
// The cursor is at a property value location like `Foo<{ x: | }`.
Expand Down