From cf06ca332013373a362951d8da1caf5748e4b540 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 17:01:54 +0200 Subject: [PATCH 01/19] Add refactoring code action `Infer function return type` --- internal/ls/codeactions.go | 85 +++++++++ .../codeactions_refactor_inferreturntype.go | 174 ++++++++++++++++++ internal/lsp/server.go | 1 + 3 files changed, 260 insertions(+) create mode 100644 internal/ls/codeactions_refactor_inferreturntype.go diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index d04f64c73d0..bd5506eb310 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -41,6 +41,7 @@ type CodeAction struct { Changes []*lsproto.TextEdit FixID string FixAllDescription string + Kind lsproto.CodeActionKind } // Compare defines a total ordering for CodeAction values, comparing description @@ -74,6 +75,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 +176,72 @@ 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 { + 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 codeActionKindContains(kind, lsproto.CodeActionKindRefactor) { + 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 + } + + changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{ + uri: action.Changes, + } + + return lsproto.CommandOrCodeAction{ + CodeAction: &lsproto.CodeAction{ + Title: action.Description, + Kind: &kind, + Edit: &lsproto.WorkspaceEdit{Changes: &changes}, + }, + } +} + // 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 00000000000..d3a508060ad --- /dev/null +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -0,0 +1,174 @@ +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) { + ch, done := refactorContext.Program.GetTypeCheckerForFile(ctx, refactorContext.SourceFile) + defer done() + + if ast.IsInJSFile(refactorContext.SourceFile.AsNode()) { + return nil, nil + } + + token := astnav.GetTokenAtPosition(refactorContext.SourceFile, int(refactorContext.Range.Pos())) + declaration := findConvertibleAncestor(token) + if declaration == nil || !hasBody(declaration) || declaration.Type() != nil { + return nil, nil + } + + returnType := getInferredReturnType(ch, declaration) + if returnType == nil { + return nil, nil + } + + formatOptions := refactorContext.LS.FormatOptions() + changeTracker := change.NewTracker(ctx, refactorContext.Program.Options(), formatOptions, refactorContext.LS.converters) + + idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol) + typeNode := ch.TypeToTypeNode(returnType, declaration, nodebuilder.FlagsNoTruncation, idToSymbol) + if typeNode == nil { + return nil, nil + } + + changeTracker.TryInsertTypeAnnotation(refactorContext.SourceFile, declaration, typeNode) + changes := changeTracker.GetChanges() + if len(changes) == 0 { + return nil, nil + } + + title := diagnostics.Infer_function_return_type.Localize(locale.FromContext(ctx)) + + actions := []*CodeAction{{ + Description: title, + Changes: changes[refactorContext.SourceFile.FileName()], + FixID: refactorID, + Kind: inferReturnTypeRefactorKind, + }} + return actions, nil +} + +// getInferredReturnType analyzes a function-like declaration and returns its +// inferred return type, handling overloads and type predicates. +func getInferredReturnType(ch *checker.Checker, declaration *ast.Node) *checker.Type { + // 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.GetUnionType(returnTypes) + } + } + } + } + + // Check for type predicate (e.g., `x is T`) + signature := ch.GetSignatureFromDeclaration(declaration) + if signature != nil { + typePredicate := ch.GetTypePredicateOfSignature(signature) + if typePredicate != nil && typePredicate.Type() != nil { + return typePredicate.Type() + } + } + + // Normal case: get the return type of the signature + if signature != nil { + return ch.GetReturnTypeOfSignature(signature) + } + + return nil +} + +// findConvertibleAncestor walks up from node to find the nearest convertible declaration. +func findConvertibleAncestor(node *ast.Node) *ast.Node { + for node != nil { + if convertibleDeclaration(node) { + return node + } + + if ast.IsVariableDeclaration(node) { + if init := node.AsVariableDeclaration().Initializer; init != nil && convertibleDeclaration(init) { + return init + } + } + + if ast.IsPropertyDeclaration(node) { + if init := node.AsPropertyDeclaration().Initializer; init != nil && convertibleDeclaration(init) { + return init + } + } + 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 6fed3a9783a..5074c138676 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, }, }, }, From 37ef7abe88442750b372bc67158e987ccfd2fe59 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 18:14:06 +0200 Subject: [PATCH 02/19] Add support `fourslash` for refactor code actions --- internal/fourslash/fourslash.go | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 39dfac4fa65..1231e48465c 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1980,6 +1980,78 @@ 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() + + 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 +} + +// VerifyRefactorAvailable verifies that a refactoring code action with the given title is available. +func (f *FourslashTest) VerifyRefactorAvailable(t *testing.T, title string) { + t.Helper() + actions := f.getRefactorActions(t) + for _, action := range actions { + if action.Title == title { + return + } + } + + t.Fatalf("Expected refactoring %q to be available, but it was not. Got: %v", title, actionTitles(actions)) +} + +// 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) + } + } +} + +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)) From ef5db47991bcf80b70e6f0437fb16bd2091bddb1 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 18:14:37 +0200 Subject: [PATCH 03/19] Add integration tests for infering return type code action refactor --- .../tests/refactorInferReturnType_test.go | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 internal/fourslash/tests/refactorInferReturnType_test.go diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go new file mode 100644 index 00000000000..25444555a4a --- /dev/null +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -0,0 +1,320 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +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.VerifyRefactorAvailable(t, inferReturnTypeTitle) +} + +// --- 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_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_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.VerifyRefactorAvailable(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) +} From be8c7b8fcfd90bbb861c658814cba85ea45ac668 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 18:59:43 +0200 Subject: [PATCH 04/19] Strenghten testing strategy with post-refactor comparison --- internal/fourslash/fourslash.go | 43 ++++- .../tests/refactorInferReturnType_test.go | 147 ++++++++++++++---- 2 files changed, 158 insertions(+), 32 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 1231e48465c..c62cba2e6a9 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2012,17 +2012,50 @@ func (f *FourslashTest) getRefactorActions(t *testing.T) []*lsproto.CodeAction { return actions } -// VerifyRefactorAvailable verifies that a refactoring code action with the given title is available. -func (f *FourslashTest) VerifyRefactorAvailable(t *testing.T, title string) { +// 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 == title { - return + 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) - t.Fatalf("Expected refactoring %q to be available, but it was not. Got: %v", title, actionTitles(actions)) + 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. diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 25444555a4a..bc74e593317 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -21,18 +21,40 @@ func TestRefactorInferReturnType_available(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + 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;` + 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.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `const f = (x: number): number => x * 2;`, + }) } func TestRefactorInferReturnType_available_methodDeclaration(t *testing.T) { @@ -47,7 +69,14 @@ func TestRefactorInferReturnType_available_methodDeclaration(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `class MyClass { + myMethod(): number { + return 42; + } +}`, + }) } func TestRefactorInferReturnType_available_overloads(t *testing.T) { @@ -62,7 +91,14 @@ function overload/*3*/(x: any) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "3") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + 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) { @@ -75,7 +111,12 @@ func TestRefactorInferReturnType_available_asyncFunction(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `async function asyncFunc(): Promise { + return 42; +}`, + }) } func TestRefactorInferReturnType_available_generatorFunction(t *testing.T) { @@ -89,7 +130,13 @@ func TestRefactorInferReturnType_available_generatorFunction(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + 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) { @@ -102,7 +149,12 @@ func TestRefactorInferReturnType_available_genericFunction(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function generic(x: T): T { + return x; +}`, + }) } func TestRefactorInferReturnType_available_methodExpression(t *testing.T) { @@ -117,7 +169,14 @@ func TestRefactorInferReturnType_available_methodExpression(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `const obj = { + myMethod(): number { + return 42; + } +};`, + }) } func TestRefactorInferReturnType_available_unionReturn(t *testing.T) { @@ -130,7 +189,12 @@ func TestRefactorInferReturnType_available_unionReturn(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + 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) { @@ -143,7 +207,16 @@ func TestRefactorInferReturnType_available_complexReturn(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + 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) { @@ -156,7 +229,12 @@ func TestRefactorInferReturnType_available_voidReturn(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `function voidFunc(): void { + console.log("hello"); +}`, + }) } func TestRefactorInferReturnType_available_exportedFunction(t *testing.T) { @@ -169,7 +247,12 @@ func TestRefactorInferReturnType_available_exportedFunction(t *testing.T) { f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `export function(): number { + return 42; +}`, + }) } func TestRefactorInferReturnType_available_defaultExportedFunction(t *testing.T) { @@ -182,7 +265,30 @@ func TestRefactorInferReturnType_available_defaultExportedFunction(t *testing.T) f, done := fourslash.NewFourslash(t, nil, content) defer done() f.GoToMarker(t, "marker") - f.VerifyRefactorAvailable(t, inferReturnTypeTitle) + f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ + Title: inferReturnTypeTitle, + NewFileContent: `export default function(): number { + return 42; +}`, + }) +} + +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; +}`, + }) } // --- Not available cases --- @@ -278,19 +384,6 @@ func TestRefactorInferReturnType_notAvailable_letVariable(t *testing.T) { f.VerifyRefactorNotAvailable(t, inferReturnTypeTitle) } -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.VerifyRefactorAvailable(t, inferReturnTypeTitle) -} - func TestRefactorInferReturnType_notAvailable_typePredicate(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") From 776d88d77ad26c0d41db8d414abbb0cbc3396ce1 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 20:55:54 +0200 Subject: [PATCH 05/19] Fix paren-less arrow handling case --- internal/fourslash/tests/refactorInferReturnType_test.go | 2 +- internal/ls/codeactions_refactor_inferreturntype.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index bc74e593317..7ce3d566f81 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -53,7 +53,7 @@ func TestRefactorInferReturnType_available_arrowFunction_parenLess(t *testing.T) f.GoToMarker(t, "marker") f.VerifyRefactor(t, fourslash.VerifyRefactorOptions{ Title: inferReturnTypeTitle, - NewFileContent: `const f = (x: number): number => x * 2;`, + NewFileContent: `const f = (x): number => x * 2;`, }) } diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index d3a508060ad..07dabc5793d 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -58,6 +58,7 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto } token := astnav.GetTokenAtPosition(refactorContext.SourceFile, int(refactorContext.Range.Pos())) + declaration := findConvertibleAncestor(token) if declaration == nil || !hasBody(declaration) || declaration.Type() != nil { return nil, nil @@ -70,14 +71,18 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto formatOptions := refactorContext.LS.FormatOptions() changeTracker := change.NewTracker(ctx, refactorContext.Program.Options(), formatOptions, refactorContext.LS.converters) - idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol) + typeNode := ch.TypeToTypeNode(returnType, declaration, nodebuilder.FlagsNoTruncation, idToSymbol) if typeNode == nil { return nil, nil } + 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 From 7db11ee5f9e03080b83daf14f32ff9b93e04c68a Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:00:18 +0200 Subject: [PATCH 06/19] Swap `codeActionKindContains` arguments --- internal/ls/codeactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index bd5506eb310..6f1e6ed091e 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -214,7 +214,7 @@ func wantsRefactors(only *[]lsproto.CodeActionKind) bool { } for _, kind := range *only { - if codeActionKindContains(kind, lsproto.CodeActionKindRefactor) { + if codeActionKindContains(lsproto.CodeActionKindRefactor, kind) { return true } } From 71a15fe08e6d20357fc811a4b6f5d3efde962fa8 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:15:45 +0200 Subject: [PATCH 07/19] Fix handling type predicates --- .../tests/refactorInferReturnType_test.go | 18 +++++++++++ .../codeactions_refactor_inferreturntype.go | 32 +++++++++---------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 7ce3d566f81..460b5089b97 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -291,6 +291,24 @@ func TestRefactorInferReturnType_available_parameterDeclaration(t *testing.T) { }) } +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"; +}`, + }) +} + // --- Not available cases --- func TestRefactorInferReturnType_notAvailable_withReturnType(t *testing.T) { diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index 07dabc5793d..0770b5bc5a6 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -64,19 +64,13 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto return nil, nil } - returnType := getInferredReturnType(ch, declaration) - if returnType == nil { + typeNode := getInferredReturnTypeNode(ch, declaration, refactorContext.SourceFile) + if typeNode == nil { return nil, nil } formatOptions := refactorContext.LS.FormatOptions() changeTracker := change.NewTracker(ctx, refactorContext.Program.Options(), formatOptions, refactorContext.LS.converters) - idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol) - - typeNode := ch.TypeToTypeNode(returnType, declaration, nodebuilder.FlagsNoTruncation, idToSymbol) - if typeNode == nil { - return nil, nil - } if ast.IsArrowFunction(declaration) { changeTracker.ParenthesizeArrowParameters(refactorContext.SourceFile, declaration) @@ -99,9 +93,11 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto return actions, nil } -// getInferredReturnType analyzes a function-like declaration and returns its -// inferred return type, handling overloads and type predicates. -func getInferredReturnType(ch *checker.Checker, declaration *ast.Node) *checker.Type { +// 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) @@ -117,24 +113,28 @@ func getInferredReturnType(ch *checker.Checker, declaration *ast.Node) *checker. } if len(returnTypes) > 0 { - return ch.GetUnionType(returnTypes) + return ch.TypeToTypeNode(ch.GetUnionType(returnTypes), declaration, nodebuilder.FlagsNoTruncation, idToSymbol) } } } } - // Check for type predicate (e.g., `x is T`) + // 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 { - return typePredicate.Type() + enclosingDecl := ast.FindAncestor(declaration, ast.IsDeclaration) + if enclosingDecl == nil { + enclosingDecl = sourceFile.AsNode() + } + return ch.TypePredicateToTypePredicateNode(typePredicate, enclosingDecl, nodebuilder.FlagsNoTruncation, idToSymbol) } } - // Normal case: get the return type of the signature + // Normal case: get the return type of the signature. if signature != nil { - return ch.GetReturnTypeOfSignature(signature) + return ch.TypeToTypeNode(ch.GetReturnTypeOfSignature(signature), declaration, nodebuilder.FlagsNoTruncation, idToSymbol) } return nil From 8263c3c6d4cc38a351ab17f68abadb6fd8cb0996 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:19:55 +0200 Subject: [PATCH 08/19] Stop ancestor walk at blocks and arrow function boundaries --- .../tests/refactorInferReturnType_test.go | 28 +++++++++++++++++++ .../codeactions_refactor_inferreturntype.go | 12 ++++++++ 2 files changed, 40 insertions(+) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 460b5089b97..01fe1d9422a 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -380,6 +380,34 @@ func TestRefactorInferReturnType_notAvailable_arrowFunctionExpression(t *testing 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") diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index 0770b5bc5a6..9ff9fcd4747 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -143,6 +143,16 @@ func getInferredReturnTypeNode(ch *checker.Checker, declaration *ast.Node, sourc // 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 } @@ -158,8 +168,10 @@ func findConvertibleAncestor(node *ast.Node) *ast.Node { return init } } + node = node.Parent } + return nil } From 4c11c494dabc263d047c343a113f64652e6f1117 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:30:23 +0200 Subject: [PATCH 09/19] Defer checker acquisition until after cheap applicability checks --- internal/ls/codeactions_refactor_inferreturntype.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index 9ff9fcd4747..d08d28ce698 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -50,9 +50,6 @@ func convertibleDeclaration(node *ast.Node) bool { // getInferReturnTypeCodeActions provides the "Infer Return Type" refactoring action. func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *RefactorContext, refactorID string) ([]*CodeAction, error) { - ch, done := refactorContext.Program.GetTypeCheckerForFile(ctx, refactorContext.SourceFile) - defer done() - if ast.IsInJSFile(refactorContext.SourceFile.AsNode()) { return nil, nil } @@ -64,6 +61,9 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto return nil, nil } + ch, done := refactorContext.Program.GetTypeCheckerForFile(ctx, refactorContext.SourceFile) + defer done() + typeNode := getInferredReturnTypeNode(ch, declaration, refactorContext.SourceFile) if typeNode == nil { return nil, nil From 31f98bad3dc4789911f54250d3ef04bf0246a4a0 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:37:27 +0200 Subject: [PATCH 10/19] Pass `AllowUnresolvedNames` to type-node builder - fixes computed properties --- .../tests/refactorInferReturnType_test.go | 17 +++++++++++++++++ .../ls/codeactions_refactor_inferreturntype.go | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 01fe1d9422a..4b813426d8c 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -273,6 +273,23 @@ func TestRefactorInferReturnType_available_defaultExportedFunction(t *testing.T) }) } +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") diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index d08d28ce698..65077a8da9d 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -113,7 +113,7 @@ func getInferredReturnTypeNode(ch *checker.Checker, declaration *ast.Node, sourc } if len(returnTypes) > 0 { - return ch.TypeToTypeNode(ch.GetUnionType(returnTypes), declaration, nodebuilder.FlagsNoTruncation, idToSymbol) + return ch.TypeToTypeNodeEx(ch.GetUnionType(returnTypes), declaration, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, idToSymbol) } } } @@ -134,7 +134,7 @@ func getInferredReturnTypeNode(ch *checker.Checker, declaration *ast.Node, sourc // Normal case: get the return type of the signature. if signature != nil { - return ch.TypeToTypeNode(ch.GetReturnTypeOfSignature(signature), declaration, nodebuilder.FlagsNoTruncation, idToSymbol) + return ch.TypeToTypeNodeEx(ch.GetReturnTypeOfSignature(signature), declaration, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, idToSymbol) } return nil From c14941be57d28b43281b9d408382c39b6e8a5c5b Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:51:58 +0200 Subject: [PATCH 11/19] Filter refactor providers by per-action Kinds against `Context.Only` --- internal/ls/codeactions.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index 6f1e6ed091e..e397835b5f0 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -191,6 +191,10 @@ func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsprot 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 @@ -222,6 +226,24 @@ func wantsRefactors(only *[]lsproto.CodeActionKind) bool { 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 From ba248b0d33c6b85f5536811a991a2c7d5f2862b8 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:54:56 +0200 Subject: [PATCH 12/19] Use `GetTouchingPropertyName` to exclude leading trivia from token lookup --- .../tests/refactorInferReturnType_test.go | 26 +++++++++++++++++++ .../codeactions_refactor_inferreturntype.go | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 4b813426d8c..0d1eae120e2 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -326,6 +326,32 @@ func TestRefactorInferReturnType_available_typePredicate(t *testing.T) { }) } +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) +} + // --- Not available cases --- func TestRefactorInferReturnType_notAvailable_withReturnType(t *testing.T) { diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index 65077a8da9d..6342be05d62 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -54,7 +54,7 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto return nil, nil } - token := astnav.GetTokenAtPosition(refactorContext.SourceFile, int(refactorContext.Range.Pos())) + token := astnav.GetTouchingPropertyName(refactorContext.SourceFile, int(refactorContext.Range.Pos())) declaration := findConvertibleAncestor(token) if declaration == nil || !hasBody(declaration) || declaration.Type() != nil { From cc7f34afe7845d5c75437268dca526ecb8329d7a Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 21:58:19 +0200 Subject: [PATCH 13/19] Use stable sort for equal-position edits in `fourslash` verifier --- internal/fourslash/fourslash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index c62cba2e6a9..10560b02ff0 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2119,7 +2119,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) From 2c5437ced82a713da56237aa71940f1273e1a543 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 22:09:49 +0200 Subject: [PATCH 14/19] Remove initializer shortcuts from `findConvertibleAncestor` --- .../tests/refactorInferReturnType_test.go | 22 +++++++++++++++++++ .../codeactions_refactor_inferreturntype.go | 12 ---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 0d1eae120e2..a6002156438 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -352,6 +352,28 @@ const f = (x: number) => x * 2;` 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) +} + // --- Not available cases --- func TestRefactorInferReturnType_notAvailable_withReturnType(t *testing.T) { diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index 6342be05d62..dd7267790fb 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -157,18 +157,6 @@ func findConvertibleAncestor(node *ast.Node) *ast.Node { return node } - if ast.IsVariableDeclaration(node) { - if init := node.AsVariableDeclaration().Initializer; init != nil && convertibleDeclaration(init) { - return init - } - } - - if ast.IsPropertyDeclaration(node) { - if init := node.AsPropertyDeclaration().Initializer; init != nil && convertibleDeclaration(init) { - return init - } - } - node = node.Parent } From 15870951bc4fb5420c4940bcb6159cdd2dd828b7 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 22:13:50 +0200 Subject: [PATCH 15/19] Add coverage for `Context.Only` filtering on refactor providers --- internal/fourslash/fourslash.go | 31 ++++++++++++++ .../tests/refactorInferReturnType_test.go | 40 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 10560b02ff0..2e83a1e21f5 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1983,6 +1983,11 @@ func (f *FourslashTest) getAllQuickFixActions(t *testing.T, errorCode ...int) [] // 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{ @@ -1994,6 +1999,7 @@ func (f *FourslashTest) getRefactorActions(t *testing.T) []*lsproto.CodeAction { }, Context: &lsproto.CodeActionContext{ Diagnostics: []*lsproto.Diagnostic{}, + Only: only, }, } @@ -2069,6 +2075,31 @@ func (f *FourslashTest) VerifyRefactorNotAvailable(t *testing.T, title string) { } } +// 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 { diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index a6002156438..6337328ad45 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -374,6 +375,45 @@ func TestRefactorInferReturnType_notAvailable_caretOnEquals(t *testing.T) { 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_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) { From e95906353c45cd67f3291c852936634c8fdd7cee Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 22:27:02 +0200 Subject: [PATCH 16/19] Add `TypePredicateToTypePredicateNodeEx` with configurable internal flags --- internal/checker/printer.go | 5 +++++ internal/ls/codeactions_refactor_inferreturntype.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/checker/printer.go b/internal/checker/printer.go index 21704cc7da9..955d92c86ea 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/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index dd7267790fb..71d0e6223e5 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -128,7 +128,7 @@ func getInferredReturnTypeNode(ch *checker.Checker, declaration *ast.Node, sourc if enclosingDecl == nil { enclosingDecl = sourceFile.AsNode() } - return ch.TypePredicateToTypePredicateNode(typePredicate, enclosingDecl, nodebuilder.FlagsNoTruncation, idToSymbol) + return ch.TypePredicateToTypePredicateNodeEx(typePredicate, enclosingDecl, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, idToSymbol) } } From 41e2c6a49dfa2a9363937a345e7c2e9325983f2f Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 22:43:36 +0200 Subject: [PATCH 17/19] Treat empty kind in `Context.Only` as matching all refactor actions --- .../fourslash/tests/refactorInferReturnType_test.go | 13 +++++++++++++ internal/ls/codeactions.go | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index 6337328ad45..e4e5a91b6a3 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -401,6 +401,19 @@ func TestRefactorInferReturnType_only_rewriteKind(t *testing.T) { 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") diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index e397835b5f0..f60cfb88cfd 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -218,7 +218,7 @@ func wantsRefactors(only *[]lsproto.CodeActionKind) bool { } for _, kind := range *only { - if codeActionKindContains(lsproto.CodeActionKindRefactor, kind) { + if kind == lsproto.CodeActionKindEmpty || codeActionKindContains(lsproto.CodeActionKindRefactor, kind) { return true } } From 7eebf25d00cadf240ee9ca5f75f2af0b4176689a Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 23:06:39 +0200 Subject: [PATCH 18/19] Return disabled code actions with not-applicable reasons --- internal/fourslash/fourslash.go | 2 +- internal/ls/codeactions.go | 21 ++++++++++++------- .../codeactions_refactor_inferreturntype.go | 15 +++++++++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 2e83a1e21f5..a55c17989c7 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2009,7 +2009,7 @@ func (f *FourslashTest) getRefactorActionsWithOnly(t *testing.T, only *[]lsproto if result.CommandOrCodeActionArray != nil { for _, item := range *result.CommandOrCodeActionArray { if item.CodeAction != nil && item.CodeAction.Kind != nil && - isRefactoringKind(*item.CodeAction.Kind) { + isRefactoringKind(*item.CodeAction.Kind) && item.CodeAction.Disabled == nil { actions = append(actions, item.CodeAction) } } diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index f60cfb88cfd..30613a9014d 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -42,6 +42,7 @@ type CodeAction struct { FixID string FixAllDescription string Kind lsproto.CodeActionKind + DisabledReason string } // Compare defines a total ordering for CodeAction values, comparing description @@ -251,16 +252,22 @@ func convertRefactorToLSPCodeAction(action *CodeAction, uri lsproto.DocumentUri) kind = lsproto.CodeActionKindRefactorRewrite } - changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{ - uri: action.Changes, + 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: &lsproto.CodeAction{ - Title: action.Description, - Kind: &kind, - Edit: &lsproto.WorkspaceEdit{Changes: &changes}, - }, + CodeAction: lspAction, } } diff --git a/internal/ls/codeactions_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index 71d0e6223e5..ee388dce882 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -54,10 +54,18 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto return nil, nil } + title := diagnostics.Infer_function_return_type.Localize(locale.FromContext(ctx)) + showNotApplicable := refactorContext.LS.UserPreferences().ProvideRefactorNotApplicableReason.IsTrue() + 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 } @@ -66,6 +74,11 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto 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 } @@ -82,8 +95,6 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto return nil, nil } - title := diagnostics.Infer_function_return_type.Localize(locale.FromContext(ctx)) - actions := []*CodeAction{{ Description: title, Changes: changes[refactorContext.SourceFile.FileName()], From c2810de5cde0c3c32481dda1755100d1e36e711f Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 26 Jul 2026 23:26:35 +0200 Subject: [PATCH 19/19] Gate disabled refactor on `DisabledSupport` client capability --- internal/fourslash/fourslash.go | 49 +++++++++++++++++++ .../tests/refactorInferReturnType_test.go | 38 ++++++++++++++ .../codeactions_refactor_inferreturntype.go | 3 +- 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index a55c17989c7..16aa90bef8f 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2075,6 +2075,55 @@ func (f *FourslashTest) VerifyRefactorNotAvailable(t *testing.T, title string) { } } +// 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) { diff --git a/internal/fourslash/tests/refactorInferReturnType_test.go b/internal/fourslash/tests/refactorInferReturnType_test.go index e4e5a91b6a3..27595d0d80e 100644 --- a/internal/fourslash/tests/refactorInferReturnType_test.go +++ b/internal/fourslash/tests/refactorInferReturnType_test.go @@ -575,3 +575,41 @@ func TestRefactorInferReturnType_notAvailable_assertionSignature(t *testing.T) { 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_refactor_inferreturntype.go b/internal/ls/codeactions_refactor_inferreturntype.go index ee388dce882..3561e7f5714 100644 --- a/internal/ls/codeactions_refactor_inferreturntype.go +++ b/internal/ls/codeactions_refactor_inferreturntype.go @@ -55,7 +55,8 @@ func getInferReturnTypeCodeActions(ctx context.Context, refactorContext *Refacto } title := diagnostics.Infer_function_return_type.Localize(locale.FromContext(ctx)) - showNotApplicable := refactorContext.LS.UserPreferences().ProvideRefactorNotApplicableReason.IsTrue() + showNotApplicable := refactorContext.LS.UserPreferences().ProvideRefactorNotApplicableReason.IsTrue() && + lsproto.GetClientCapabilities(ctx).TextDocument.CodeAction.DisabledSupport token := astnav.GetTouchingPropertyName(refactorContext.SourceFile, int(refactorContext.Range.Pos()))