diff --git a/internal/checker/services.go b/internal/checker/services.go index cec934f547..05236b85f0 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -671,6 +671,18 @@ func (c *Checker) GetContextualDeclarationsForObjectLiteralElement(objectLiteral return result } +// GetContextualTypeForArrayElement returns the contextual type for an element at the given index +// in an array with the given contextual type. +func (c *Checker) GetContextualTypeForArrayElement(contextualArrayType *Type, elementIndex int) *Type { + if contextualArrayType == nil { + return nil + } + // Pass -1 for length, firstSpreadIndex, and lastSpreadIndex since we don't have + // access to the actual array literal. This falls back to getting the iterated type + // or checking numeric properties, which is appropriate for completion contexts. + return c.getContextualTypeForElementExpression(contextualArrayType, elementIndex, -1, -1, -1) +} + var knownGenericTypeNames = map[string]struct{}{ "Array": {}, "ArrayLike": {}, diff --git a/internal/fourslash/tests/manual/completionsInArrayLiteralWithContextualType_test.go b/internal/fourslash/tests/manual/completionsInArrayLiteralWithContextualType_test.go new file mode 100644 index 0000000000..89e53169ff --- /dev/null +++ b/internal/fourslash/tests/manual/completionsInArrayLiteralWithContextualType_test.go @@ -0,0 +1,51 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/testutil" +) + +// Test that string literal completions are suggested in tuple contexts +// even without typing a quote character. +func TestCompletionsInArrayLiteralWithContextualType(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + // Test 1: Completions after `[` in a tuple should suggest string literals + const content1 = `let y: ["foo" | "bar", string] = [/*a*/];` + f1, done1 := fourslash.NewFourslash(t, nil /*capabilities*/, content1) + defer done1() + f1.VerifyCompletions(t, "a", &fourslash.CompletionsExpectedList{ + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "\"foo\"", + "\"bar\"", + }, + }, + }) + + // Test 2: Completions after `,` in a tuple should provide contextual type for second element + const content2 = `let z: ["a", "b" | "c"] = ["a", /*b*/];` + f2, done2 := fourslash.NewFourslash(t, nil /*capabilities*/, content2) + defer done2() + f2.VerifyCompletions(t, "b", &fourslash.CompletionsExpectedList{ + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "\"b\"", + "\"c\"", + }, + }, + }) +} diff --git a/internal/fourslash/tests/manual/completionsInMapConstructorNoCrash_test.go b/internal/fourslash/tests/manual/completionsInMapConstructorNoCrash_test.go new file mode 100644 index 0000000000..73acc7a00a --- /dev/null +++ b/internal/fourslash/tests/manual/completionsInMapConstructorNoCrash_test.go @@ -0,0 +1,49 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/testutil" +) + +// Test for issue: Completions crash in call to `new Map(...)`. +// When requesting completions inside a Map constructor's array literal, +// IndexOfNode returns -1 causing a panic in getContextualTypeForElementExpression +// when trying to access array elements without a bounds check. +func TestCompletionsInMapConstructorNoCrash(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + // Test completion at position /*a*/ - before the string literal + const content1 = `const m = new Map([ + [/*a*/'0', ['0', false]], +]);` + f1, done1 := fourslash.NewFourslash(t, nil /*capabilities*/, content1) + defer done1() + // Just verify that completions don't crash - accept any completion list + f1.VerifyCompletions(t, "a", &fourslash.CompletionsExpectedList{ + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{}, + }) + + // Test completion at position /*b*/ - after the array literal + const content2 = `const m = new Map([ + ['0', ['0', false]]/*b*/, +]);` + f2, done2 := fourslash.NewFourslash(t, nil /*capabilities*/, content2) + defer done2() + // Just verify that completions don't crash - accept any completion list + f2.VerifyCompletions(t, "b", &fourslash.CompletionsExpectedList{ + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{}, + }) +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 905eb8e174..a7f38f4afd 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -2967,6 +2967,37 @@ func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFi return typeChecker.GetContextualTypeForJsxAttribute(parent.Parent) } return nil + case ast.KindOpenBracketToken: + // When completing after `[` in an array literal (e.g., `[/*here*/]`), + // we should provide contextual type for the first element + if ast.IsArrayLiteralExpression(parent) { + contextualArrayType := typeChecker.GetContextualType(parent, checker.ContextFlagsNone) + if contextualArrayType != nil { + // Get the type for the first element (index 0) + return typeChecker.GetContextualTypeForArrayElement(contextualArrayType, 0) + } + } + return nil + case ast.KindCommaToken: + // When completing after `,` in an array literal (e.g., `[x, /*here*/]`), + // we should provide contextual type for the element after the comma + if ast.IsArrayLiteralExpression(parent) { + contextualArrayType := typeChecker.GetContextualType(parent, checker.ContextFlagsNone) + if contextualArrayType != nil { + // Count how many elements come before the cursor position + arrayLiteral := parent.AsArrayLiteralExpression() + elementIndex := 0 + for _, elem := range arrayLiteral.Elements.Nodes { + if elem.Pos() < position { + elementIndex++ + } else { + break + } + } + return typeChecker.GetContextualTypeForArrayElement(contextualArrayType, elementIndex) + } + } + return nil case ast.KindQuestionToken: // When completing after `?` in a ternary conditional (e.g., `foo(a ? /*here*/)`), // we need to look at the parent conditional expression to find the contextual type.