diff --git a/internal/checker/printer.go b/internal/checker/printer.go index 21704cc7da..955d92c86e 100644 --- a/internal/checker/printer.go +++ b/internal/checker/printer.go @@ -484,3 +484,8 @@ func (c *Checker) TypePredicateToTypePredicateNode(t *TypePredicate, enclosingDe nodeBuilder := c.getNodeBuilderEx(idToSymbol) return nodeBuilder.TypePredicateToTypePredicateNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil) } + +func (c *Checker) TypePredicateToTypePredicateNodeEx(t *TypePredicate, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypePredicateNodeNode { + nodeBuilder := c.getNodeBuilderEx(idToSymbol) + return nodeBuilder.TypePredicateToTypePredicateNode(t, enclosingDeclaration, flags, internalFlags, nil) +} diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 39dfac4fa6..16aa90bef8 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1980,6 +1980,191 @@ func (f *FourslashTest) getAllQuickFixActions(t *testing.T, errorCode ...int) [] return actions } +// getRefactorActions gets all refactoring code actions at the current cursor position. +func (f *FourslashTest) getRefactorActions(t *testing.T) []*lsproto.CodeAction { + t.Helper() + return f.getRefactorActionsWithOnly(t, nil) +} + +func (f *FourslashTest) getRefactorActionsWithOnly(t *testing.T, only *[]lsproto.CodeActionKind) []*lsproto.CodeAction { + t.Helper() + + params := &lsproto.CodeActionParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + Range: lsproto.Range{ + Start: f.currentCaretPosition, + End: f.currentCaretPosition, + }, + Context: &lsproto.CodeActionContext{ + Diagnostics: []*lsproto.Diagnostic{}, + Only: only, + }, + } + + result := sendRequest(t, f, lsproto.TextDocumentCodeActionInfo, params) + + var actions []*lsproto.CodeAction + if result.CommandOrCodeActionArray != nil { + for _, item := range *result.CommandOrCodeActionArray { + if item.CodeAction != nil && item.CodeAction.Kind != nil && + isRefactoringKind(*item.CodeAction.Kind) && item.CodeAction.Disabled == nil { + actions = append(actions, item.CodeAction) + } + } + } + + return actions +} + +// VerifyRefactorOptions contains options for VerifyRefactor. +type VerifyRefactorOptions struct { + Title string + NewFileContent string +} + +// VerifyRefactor verifies that a refactoring code action matching the given title is available, +// and optionally verifies the file content after applying the edit. +func (f *FourslashTest) VerifyRefactor(t *testing.T, options VerifyRefactorOptions) { + t.Helper() + actions := f.getRefactorActions(t) + + var matchingAction *lsproto.CodeAction + for _, action := range actions { + if action.Title == options.Title { + matchingAction = action + break + } + } + if matchingAction == nil { + t.Fatalf("Expected refactoring %q to be available, but it was not. Got: %v", options.Title, actionTitles(actions)) + } + + if options.NewFileContent != "" { + actual := f.getScriptInfo(f.activeFilename).content + if matchingAction.Edit != nil && matchingAction.Edit.Changes != nil { + expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename) + + for uri, edits := range *matchingAction.Edit.Changes { + if uri != expectedURI { + t.Fatalf("Refactoring returned edits for unexpected URI %q (expected %q)", uri, expectedURI) + } + + actual = f.applyEditsToContent(actual, edits) + } + } + assert.Equal(t, options.NewFileContent, actual, "File content after applying refactoring did not match expected content.") + } +} + +// VerifyRefactorAvailable verifies that a refactoring code action with the given title is available. +func (f *FourslashTest) VerifyRefactorAvailable(t *testing.T, title string) { + t.Helper() + f.VerifyRefactor(t, VerifyRefactorOptions{Title: title}) +} + +// VerifyRefactorNotAvailable verifies that a refactoring code action with the given title is NOT available. +func (f *FourslashTest) VerifyRefactorNotAvailable(t *testing.T, title string) { + t.Helper() + actions := f.getRefactorActions(t) + for _, action := range actions { + if action.Title == title { + t.Fatalf("Expected refactoring %q to not be available, but it was", title) + } + } +} + +// getRefactorActionsIncludingDisabled returns all refactoring code actions at the current cursor position, +// including disabled ones. This is used to test the DisabledSupport client capability. +func (f *FourslashTest) getRefactorActionsIncludingDisabled(t *testing.T) []*lsproto.CodeAction { + t.Helper() + + params := &lsproto.CodeActionParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + Range: lsproto.Range{ + Start: f.currentCaretPosition, + End: f.currentCaretPosition, + }, + Context: &lsproto.CodeActionContext{ + Diagnostics: []*lsproto.Diagnostic{}, + }, + } + + result := sendRequest(t, f, lsproto.TextDocumentCodeActionInfo, params) + + var actions []*lsproto.CodeAction + if result.CommandOrCodeActionArray != nil { + for _, item := range *result.CommandOrCodeActionArray { + if item.CodeAction != nil && item.CodeAction.Kind != nil && + isRefactoringKind(*item.CodeAction.Kind) { + actions = append(actions, item.CodeAction) + } + } + } + + return actions +} + +// VerifyRefactorDisabled verifies that a refactoring code action with the given title IS returned +// but with its Disabled field set (i.e. the action is not applicable). +func (f *FourslashTest) VerifyRefactorDisabled(t *testing.T, title string) { + t.Helper() + actions := f.getRefactorActionsIncludingDisabled(t) + for _, action := range actions { + if action.Title == title { + if action.Disabled == nil { + t.Fatalf("Expected refactoring %q to be disabled, but it was enabled", title) + } + return + } + } + t.Fatalf("Expected refactoring %q to be present (disabled), but it was not found. Got: %v", title, actionTitles(actions)) +} + +// VerifyRefactorWithOnlyAvailable verifies that a refactoring action with the given title IS available +// when the given Only filter is sent in the request context. +func (f *FourslashTest) VerifyRefactorWithOnlyAvailable(t *testing.T, title string, only []lsproto.CodeActionKind) { + t.Helper() + actions := f.getRefactorActionsWithOnly(t, &only) + for _, action := range actions { + if action.Title == title { + return + } + } + t.Fatalf("Expected refactoring %q to be available with Only=%v, but it was not (got: %v)", title, only, actionTitles(actions)) +} + +// VerifyRefactorWithOnlyNotAvailable verifies that a refactoring action with the given title is NOT available +// when the given Only filter is sent in the request context. +func (f *FourslashTest) VerifyRefactorWithOnlyNotAvailable(t *testing.T, title string, only []lsproto.CodeActionKind) { + t.Helper() + actions := f.getRefactorActionsWithOnly(t, &only) + for _, action := range actions { + if action.Title == title { + t.Fatalf("Expected refactoring %q to not be available with Only=%v, but it was", title, only) + } + } +} + +func actionTitles(actions []*lsproto.CodeAction) []string { + titles := make([]string, len(actions)) + for i, a := range actions { + titles[i] = a.Title + } + return titles +} + +// isRefactoringKind checks if the given kind is a refactoring kind. +// Matches "refactor" and any subkind like "refactor.rewrite", "refactor.extract", etc. +func isRefactoringKind(kind lsproto.CodeActionKind) bool { + return kind == lsproto.CodeActionKindRefactor || + string(kind) == "refactor" || + strings.HasPrefix(string(kind), string(lsproto.CodeActionKindRefactor)+".") +} + func (f *FourslashTest) updateTextRangeForTextEdits(textRange core.TextRange, edits []*lsproto.TextEdit) core.TextRange { script := f.getScriptInfo(f.activeFilename) spans := make([]textEditSpan, 0, len(edits)) @@ -2014,7 +2199,7 @@ func (f *FourslashTest) updateTextRangeForTextEdits(textRange core.TextRange, ed // applyEditsToContent applies text edits to a content string without mutating the file. func (f *FourslashTest) applyEditsToContent(content string, edits []*lsproto.TextEdit) string { script := f.getScriptInfo(f.activeFilename) - slices.SortFunc(edits, func(a, b *lsproto.TextEdit) int { + slices.SortStableFunc(edits, func(a, b *lsproto.TextEdit) int { aStart := f.converters.LineAndCharacterToPosition(script, a.Range.Start) bStart := f.converters.LineAndCharacterToPosition(script, b.Range.Start) return int(aStart) - int(bStart) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go new file mode 100644 index 0000000000..27595d0d80 --- /dev/null +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -0,0 +1,615 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +const inferReturnTypeTitle = "Infer function return type" + +// --- Available cases --- + +func TestRefactorInferReturnType_available(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function simple/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function simple(): number { + return 42; +}`, + }) +} + +func TestRefactorInferReturnType_available_arrowFunction(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const arrow = /*marker*/(x: number) => x * 2;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `const arrow = (x: number): number => x * 2;`, + }) +} + +func TestRefactorInferReturnType_available_arrowFunction_parenLess(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const f = /*marker*/x => x * 2;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `const f = (x): number => x * 2;`, + }) +} + +func TestRefactorInferReturnType_available_methodDeclaration(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `class MyClass { + myMethod/*marker*/() { + return 42; + } +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `class MyClass { + myMethod(): number { + return 42; + } +}`, + }) +} + +func TestRefactorInferReturnType_available_overloads(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function overload/*1*/(x: number): number; +function overload/*2*/(x: string): string; +function overload/*3*/(x: any) { + return x; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "3") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function overload(x: number): number; +function overload(x: string): string; +function overload(x: any): string | number { + return x; +}`, + }) +} + +func TestRefactorInferReturnType_available_asyncFunction(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `async function asyncFunc/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `async function asyncFunc(): Promise { + return 42; +}`, + }) +} + +func TestRefactorInferReturnType_available_generatorFunction(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function* generator/*marker*/() { + yield 1; + yield 2; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function* generator(): Generator<1 | 2, void, unknown> { + yield 1; + yield 2; +}`, + }) +} + +func TestRefactorInferReturnType_available_genericFunction(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function generic/*marker*/(x: T) { + return x; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function generic(x: T): T { + return x; +}`, + }) +} + +func TestRefactorInferReturnType_available_methodExpression(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const obj = { + myMethod/*marker*/() { + return 42; + } +};` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `const obj = { + myMethod(): number { + return 42; + } +};`, + }) +} + +func TestRefactorInferReturnType_available_unionReturn(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function union/*marker*/(flag: boolean) { + return flag ? 42 : "hello"; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function union(flag: boolean): "hello" | 42 { + return flag ? 42 : "hello"; +}`, + }) +} + +func TestRefactorInferReturnType_available_complexReturn(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function complex/*marker*/() { + return { a: 1, b: "hello", c: [1, 2, 3] }; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function complex(): { + a: number; + b: string; + c: number[]; +} { + return { a: 1, b: "hello", c: [1, 2, 3] }; +}`, + }) +} + +func TestRefactorInferReturnType_available_voidReturn(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function voidFunc/*marker*/() { + console.log("hello"); +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function voidFunc(): void { + console.log("hello"); +}`, + }) +} + +func TestRefactorInferReturnType_available_exportedFunction(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `export function/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `export function(): number { + return 42; +}`, + }) +} + +func TestRefactorInferReturnType_available_defaultExportedFunction(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `export default function/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `export default function(): number { + return 42; +}`, + }) +} + +func TestRefactorInferReturnType_available_computedProperty(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const key = "foo"; +function withComputed/*marker*/() { + return { [key]: 42 }; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: "const key = \"foo\";\nfunction withComputed(): {\n foo: number;\n} {\n return { [key]: 42 };\n}", + }) +} + +func TestRefactorInferReturnType_available_parameterDeclaration(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function func(x/*marker*/) { + return x; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function func(x): any { + return x; +}`, + }) +} + +func TestRefactorInferReturnType_available_typePredicate(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function isString/*marker*/(x: unknown) { + return typeof x === "string"; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function isString(x: unknown): x is string { + return typeof x === "string"; +}`, + }) +} + +func TestRefactorInferReturnType_notAvailable_cursorOnLeadingTrivia(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `/*marker*/ +function hello() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_cursorOnLeadingTriviaBeforeArrow(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `/*marker*/ +const f = (x: number) => x * 2;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_caretOnVariableName(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const /*marker*/f = () => 1;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_caretOnEquals(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const f =/*marker*/ () => 1;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_only_exactKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function simple/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorWithOnlyAvailable(t, inferReturnTypeTitle, []lsproto.CodeActionKind{"refactor.rewrite.function.returnType"}) +} + +func TestRefactorInferReturnType_only_rewriteKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function simple/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorWithOnlyAvailable(t, inferReturnTypeTitle, []lsproto.CodeActionKind{"refactor.rewrite"}) +} + +func TestRefactorInferReturnType_only_emptyKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function simple/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorWithOnlyAvailable(t, inferReturnTypeTitle, []lsproto.CodeActionKind{""}) +} + +func TestRefactorInferReturnType_only_extractKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function simple/*marker*/() { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorWithOnlyNotAvailable(t, inferReturnTypeTitle, []lsproto.CodeActionKind{"refactor.extract"}) +} + +// --- Not available cases --- + +func TestRefactorInferReturnType_notAvailable_withReturnType(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function simple/*marker*/(): number { + return 42; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_constructor(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `class MyClass { + constructor/*marker*/(x: number) { + } +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_getAccessor(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `class MyClass { + get myProp/*marker*/() { + return 42; + } +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_setAccessor(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `class MyClass { + set myProp/*marker*/(value: number) { + } +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_arrowFunctionExpression(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const obj = { + myMethod/*marker*/: (x: number) => x * 2 +};` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_cursorInsideFunctionBody(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function outer() { + const inner/*marker*/ = 42; + return inner; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_cursorInsideArrowBody(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const fn = () => { + const x/*marker*/ = 42; + return x; +};` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_constVariable(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `const myConst/*marker*/ = 42;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_letVariable(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `let myLet/*marker*/ = 42;` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_typePredicate(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function isString/*marker*/(x: any): x is string { + return typeof x === "string"; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_notAvailable_assertionSignature(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `function assertString/*marker*/(x: any): asserts x is string { + if (typeof x !== "string") { + throw new Error("Not a string"); + } +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} + +// --- Disabled action tests (requires DisabledSupport client capability) --- + +func TestRefactorInferReturnType_disabled_withCapability(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + // Case: function already has an explicit return type — not applicable. + const content = `function isString/*marker*/(x: any): x is string { + return typeof x === "string"; +}` + ptrTrue := true + capabilities := &lsproto.ClientCapabilities{ + TextDocument: &lsproto.TextDocumentClientCapabilities{ + CodeAction: &lsproto.CodeActionClientCapabilities{ + DisabledSupport: &ptrTrue, + }, + }, + } + f, done := fourslash.NewFourslash(t, capabilities, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorDisabled(t, inferReturnTypeTitle) +} + +func TestRefactorInferReturnType_disabled_withoutCapability(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + // Without DisabledSupport capability, disabled action should NOT be returned at all. + const content = `function isString/*marker*/(x: any): x is string { + return typeof x === "string"; +}` + f, done := fourslash.NewFourslash(t, nil, content) + defer done() + f.GoToMarker(t, "marker") + f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) +} diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index d04f64c73d..30613a9014 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -41,6 +41,8 @@ type CodeAction struct { Changes []*lsproto.TextEdit FixID string FixAllDescription string + Kind lsproto.CodeActionKind + DisabledReason string } // Compare defines a total ordering for CodeAction values, comparing description @@ -74,6 +76,27 @@ var codeFixProviders = []*CodeFixProvider{ // Add more code fix providers here as they are implemented } +// RefactorActionFactory is a function that produces refactoring code actions. +type RefactorActionFactory func(ctx context.Context, refactorContext *RefactorContext, refactorID string) ([]*CodeAction, error) + +// RefactorAction describes a single refactoring action offered by a RefactorProvider. +type RefactorAction struct { + Title string + ID string + Kinds []lsproto.CodeActionKind + GetActions RefactorActionFactory +} + +// RefactorProvider represents a provider for refactoring code actions. +type RefactorProvider struct { + RefactorActions []RefactorAction +} + +// refactorProviders is the list of all registered refactoring providers. +var refactorProviders = []*RefactorProvider{ + InferReturnTypeProvider, +} + // ProvideCodeActions returns code actions for the given range and context func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsproto.CodeActionParams) (lsproto.CodeActionResponse, error) { program, file := l.getProgramAndFile(params.TextDocument.Uri) @@ -154,9 +177,100 @@ func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsprot actions = append(actions, fixAllActions...) } + // Refactoring actions (contextual, not diagnostic-driven) + if params.Context != nil && wantsRefactors(params.Context.Only) { + refactorContext := &RefactorContext{ + SourceFile: file, + Range: core.NewTextRange( + int(l.converters.LineAndCharacterToPosition(file, params.Range.Start)), + int(l.converters.LineAndCharacterToPosition(file, params.Range.End)), + ), + Program: program, + LS: l, + Params: params, + } + + for _, provider := range refactorProviders { + for _, action := range provider.RefactorActions { + if !refactorActionMatchesOnly(action, params.Context.Only) { + continue + } + + providerActions, err := action.GetActions(ctx, refactorContext, action.ID) + if err != nil { + return lsproto.CodeActionResponse{}, err + } + + for _, a := range providerActions { + actions = append(actions, convertRefactorToLSPCodeAction(a, params.TextDocument.Uri)) + } + } + } + } + return lsproto.CommandOrCodeActionArrayOrNull{CommandOrCodeActionArray: &actions}, nil } +// wantsRefactors returns true if the Only filter is nil/empty (meaning all kinds are wanted) +// or explicitly includes any refactor kind. +func wantsRefactors(only *[]lsproto.CodeActionKind) bool { + if only == nil || len(*only) == 0 { + return true + } + + for _, kind := range *only { + if kind == lsproto.CodeActionKindEmpty || codeActionKindContains(lsproto.CodeActionKindRefactor, kind) { + return true + } + } + + return false +} + +// refactorActionMatchesOnly returns true if the action's Kinds match any of the requested Only kinds. +// If Only is nil/empty, all actions match. +func refactorActionMatchesOnly(action RefactorAction, only *[]lsproto.CodeActionKind) bool { + if only == nil || len(*only) == 0 { + return true + } + + for _, requestedKind := range *only { + for _, actionKind := range action.Kinds { + if codeActionKindContains(requestedKind, actionKind) { + return true + } + } + } + + return false +} + +// convertRefactorToLSPCodeAction converts an internal CodeAction to an LSP CodeAction for refactoring. +func convertRefactorToLSPCodeAction(action *CodeAction, uri lsproto.DocumentUri) lsproto.CommandOrCodeAction { + kind := action.Kind + if kind == "" { + kind = lsproto.CodeActionKindRefactorRewrite + } + + lspAction := &lsproto.CodeAction{ + Title: action.Description, + Kind: &kind, + } + + if action.DisabledReason != "" { + lspAction.Disabled = &lsproto.CodeActionDisabled{Reason: action.DisabledReason} + } else { + changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{ + uri: action.Changes, + } + lspAction.Edit = &lsproto.WorkspaceEdit{Changes: &changes} + } + + return lsproto.CommandOrCodeAction{ + CodeAction: lspAction, + } +} + // getFixAllQuickFixes returns per-provider "Fix all in file" quickfix entries for providers // that matched at least 2 diagnostics in the full file. func (l *LanguageService) getFixAllQuickFixes( diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go new file mode 100644 index 0000000000..3561e7f571 --- /dev/null +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -0,0 +1,191 @@ +package ls + +import ( + "context" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/diagnostics" + "github.com/microsoft/typescript-go/internal/locale" + "github.com/microsoft/typescript-go/internal/ls/change" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/nodebuilder" +) + +const inferReturnTypeRefactorKind = lsproto.CodeActionKind("refactor.rewrite.function.returnType") + +// InferReturnTypeProvider is a RefactorProvider that adds return type annotations +// by inferring the type from the function body. +var InferReturnTypeProvider = &RefactorProvider{ + RefactorActions: []RefactorAction{ + { + Title: "Infer function return type", + ID: "inferReturnType", + Kinds: []lsproto.CodeActionKind{inferReturnTypeRefactorKind}, + GetActions: getInferReturnTypeCodeActions, + }, + }, +} + +// RefactorContext contains the context needed to generate refactoring actions. +type RefactorContext struct { + SourceFile *ast.SourceFile + Range core.TextRange + Program *compiler.Program + LS *LanguageService + Params *lsproto.CodeActionParams +} + +// convertibleDeclaration returns true if the node is a function-like declaration +// that can have its return type inferred (matches TypeScript's ConvertibleDeclaration). +func convertibleDeclaration(node *ast.Node) bool { + return ast.IsFunctionLikeDeclaration(node) && + !ast.IsConstructorDeclaration(node) && + !ast.IsGetAccessorDeclaration(node) && + !ast.IsSetAccessorDeclaration(node) +} + +// getInferReturnTypeCodeActions provides the "Infer Return Type" refactoring action. +func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *RefactorContext, refactorID string) ([]*CodeAction, error) { + if ast.IsInJSFile(refactorContext.SourceFile.AsNode()) { + return nil, nil + } + + title := diagnostics.Infer_function_return_type.Localize(locale.FromContext(ctx)) + showNotApplicable := refactorContext.LS.UserPreferences().ProvideRefactorNotApplicableReason.IsTrue() && + lsproto.GetClientCapabilities(ctx).TextDocument.CodeAction.DisabledSupport + + token := astnav.GetTouchingPropertyName(refactorContext.SourceFile, int(refactorContext.Range.Pos())) + + declaration := findConvertibleAncestor(token) + if declaration == nil || !hasBody(declaration) || declaration.Type() != nil { + if showNotApplicable { + reason := diagnostics.Return_type_must_be_inferred_from_a_function.Localize(locale.FromContext(ctx)) + return []*CodeAction{{Description: title, Kind: inferReturnTypeRefactorKind, DisabledReason: reason}}, nil + } + + return nil, nil + } + + ch, done := refactorContext.Program.GetTypeCheckerForFile(ctx, refactorContext.SourceFile) + defer done() + + typeNode := getInferredReturnTypeNode(ch, declaration, refactorContext.SourceFile) + if typeNode == nil { + if showNotApplicable { + reason := diagnostics.Could_not_determine_function_return_type.Localize(locale.FromContext(ctx)) + return []*CodeAction{{Description: title, Kind: inferReturnTypeRefactorKind, DisabledReason: reason}}, nil + } + + return nil, nil + } + + formatOptions := refactorContext.LS.FormatOptions() + changeTracker := change.NewTracker(ctx, refactorContext.Program.Options(), formatOptions, refactorContext.LS.converters) + + if ast.IsArrowFunction(declaration) { + changeTracker.ParenthesizeArrowParameters(refactorContext.SourceFile, declaration) + } + changeTracker.TryInsertTypeAnnotation(refactorContext.SourceFile, declaration, typeNode) + + changes := changeTracker.GetChanges() + if len(changes) == 0 { + return nil, nil + } + + actions := []*CodeAction{{ + Description: title, + Changes: changes[refactorContext.SourceFile.FileName()], + FixID: refactorID, + Kind: inferReturnTypeRefactorKind, + }} + return actions, nil +} + +// getInferredReturnTypeNode analyzes a function-like declaration and returns its +// inferred return type node, handling overloads and type predicates. +func getInferredReturnTypeNode(ch *checker.Checker, declaration *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol) + + // Handle overloaded function implementations: union return types of all signatures. + if ch.GetEmitResolver().IsImplementationOfOverload(declaration) { + fnType := ch.GetTypeAtLocation(declaration) + if fnType != nil { + signatures := ch.GetCallSignatures(fnType) + if len(signatures) > 1 { + returnTypes := make([]*checker.Type, 0, len(signatures)) + for _, sig := range signatures { + rt := ch.GetReturnTypeOfSignature(sig) + if rt != nil { + returnTypes = append(returnTypes, rt) + } + } + + if len(returnTypes) > 0 { + return ch.TypeToTypeNodeEx(ch.GetUnionType(returnTypes), declaration, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, idToSymbol) + } + } + } + } + + // Check for type predicate (e.g., `x is T`): build the predicate node directly. + signature := ch.GetSignatureFromDeclaration(declaration) + if signature != nil { + typePredicate := ch.GetTypePredicateOfSignature(signature) + if typePredicate != nil && typePredicate.Type() != nil { + enclosingDecl := ast.FindAncestor(declaration, ast.IsDeclaration) + if enclosingDecl == nil { + enclosingDecl = sourceFile.AsNode() + } + return ch.TypePredicateToTypePredicateNodeEx(typePredicate, enclosingDecl, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, idToSymbol) + } + } + + // Normal case: get the return type of the signature. + if signature != nil { + return ch.TypeToTypeNodeEx(ch.GetReturnTypeOfSignature(signature), declaration, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, idToSymbol) + } + + return nil +} + +// findConvertibleAncestor walks up from node to find the nearest convertible declaration. +func findConvertibleAncestor(node *ast.Node) *ast.Node { + for node != nil { + if ast.IsBlock(node) { + return nil + } + + if node.Parent != nil && ast.IsArrowFunction(node.Parent) { + if node.Kind == ast.KindEqualsGreaterThanToken || node.Parent.AsArrowFunction().Body == node { + return nil + } + } + + if convertibleDeclaration(node) { + return node + } + + node = node.Parent + } + + return nil +} + +// hasBody returns true if the node has a function body. +func hasBody(node *ast.Node) bool { + switch node.Kind { + case ast.KindFunctionDeclaration: + return node.AsFunctionDeclaration().Body != nil + case ast.KindFunctionExpression: + return node.AsFunctionExpression().Body != nil + case ast.KindArrowFunction: + return node.AsArrowFunction().Body != nil + case ast.KindMethodDeclaration: + return node.AsMethodDeclaration().Body != nil + } + return false +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 6fed3a9783..5074c13867 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1184,6 +1184,7 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ lsproto.CodeActionKindSourceRemoveUnusedImports, lsproto.CodeActionKindSourceSortImports, lsproto.CodeActionKindSourceFixAll, + lsproto.CodeActionKindRefactorRewrite, }, }, },