golang.org/x/tools/gopls@v0.15.3/internal/golang/codeaction.go (about) 1 // Copyright 2024 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package golang 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "go/ast" 12 "strings" 13 14 "golang.org/x/tools/go/ast/inspector" 15 "golang.org/x/tools/gopls/internal/analysis/fillstruct" 16 "golang.org/x/tools/gopls/internal/cache" 17 "golang.org/x/tools/gopls/internal/cache/parsego" 18 "golang.org/x/tools/gopls/internal/file" 19 "golang.org/x/tools/gopls/internal/protocol" 20 "golang.org/x/tools/gopls/internal/protocol/command" 21 "golang.org/x/tools/gopls/internal/settings" 22 "golang.org/x/tools/gopls/internal/util/bug" 23 "golang.org/x/tools/gopls/internal/util/slices" 24 "golang.org/x/tools/internal/event" 25 "golang.org/x/tools/internal/event/tag" 26 "golang.org/x/tools/internal/imports" 27 ) 28 29 // CodeActions returns all code actions (edits and other commands) 30 // available for the selected range. 31 func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) (actions []protocol.CodeAction, _ error) { 32 // Only compute quick fixes if there are any diagnostics to fix. 33 wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0 34 35 // Code actions requiring syntax information alone. 36 if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] { 37 pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) 38 if err != nil { 39 return nil, err 40 } 41 42 // Process any missing imports and pair them with the diagnostics they fix. 43 if wantQuickFixes || want[protocol.SourceOrganizeImports] { 44 importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf) 45 if err != nil { 46 event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Path())) 47 importEdits = nil 48 importEditsPerFix = nil 49 } 50 51 // Separate this into a set of codeActions per diagnostic, where 52 // each action is the addition, removal, or renaming of one import. 53 if wantQuickFixes { 54 for _, importFix := range importEditsPerFix { 55 fixed := fixedByImportFix(importFix.fix, diagnostics) 56 if len(fixed) == 0 { 57 continue 58 } 59 actions = append(actions, protocol.CodeAction{ 60 Title: importFixTitle(importFix.fix), 61 Kind: protocol.QuickFix, 62 Edit: &protocol.WorkspaceEdit{ 63 DocumentChanges: documentChanges(fh, importFix.edits), 64 }, 65 Diagnostics: fixed, 66 }) 67 } 68 } 69 70 // Send all of the import edits as one code action if the file is 71 // being organized. 72 if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 { 73 actions = append(actions, protocol.CodeAction{ 74 Title: "Organize Imports", 75 Kind: protocol.SourceOrganizeImports, 76 Edit: &protocol.WorkspaceEdit{ 77 DocumentChanges: documentChanges(fh, importEdits), 78 }, 79 }) 80 } 81 } 82 83 if want[protocol.RefactorExtract] { 84 extractions, err := getExtractCodeActions(pgf, rng, snapshot.Options()) 85 if err != nil { 86 return nil, err 87 } 88 actions = append(actions, extractions...) 89 } 90 } 91 92 // Code actions requiring type information. 93 if want[protocol.RefactorRewrite] || 94 want[protocol.RefactorInline] || 95 want[protocol.GoTest] { 96 pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) 97 if err != nil { 98 return nil, err 99 } 100 if want[protocol.RefactorRewrite] { 101 rewrites, err := getRewriteCodeActions(pkg, pgf, fh, rng, snapshot.Options()) 102 if err != nil { 103 return nil, err 104 } 105 actions = append(actions, rewrites...) 106 } 107 108 if want[protocol.RefactorInline] { 109 rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options()) 110 if err != nil { 111 return nil, err 112 } 113 actions = append(actions, rewrites...) 114 } 115 116 if want[protocol.GoTest] { 117 fixes, err := getGoTestCodeActions(pkg, pgf, rng) 118 if err != nil { 119 return nil, err 120 } 121 actions = append(actions, fixes...) 122 } 123 } 124 return actions, nil 125 } 126 127 func supportsResolveEdits(options *settings.Options) bool { 128 return options.CodeActionResolveOptions != nil && slices.Contains(options.CodeActionResolveOptions, "edit") 129 } 130 131 func importFixTitle(fix *imports.ImportFix) string { 132 var str string 133 switch fix.FixType { 134 case imports.AddImport: 135 str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) 136 case imports.DeleteImport: 137 str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) 138 case imports.SetImportName: 139 str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) 140 } 141 return str 142 } 143 144 // fixedByImportFix filters the provided slice of diagnostics to those that 145 // would be fixed by the provided imports fix. 146 func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic { 147 var results []protocol.Diagnostic 148 for _, diagnostic := range diagnostics { 149 switch { 150 // "undeclared name: X" may be an unresolved import. 151 case strings.HasPrefix(diagnostic.Message, "undeclared name: "): 152 ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ") 153 if ident == fix.IdentName { 154 results = append(results, diagnostic) 155 } 156 // "undefined: X" may be an unresolved import at Go 1.20+. 157 case strings.HasPrefix(diagnostic.Message, "undefined: "): 158 ident := strings.TrimPrefix(diagnostic.Message, "undefined: ") 159 if ident == fix.IdentName { 160 results = append(results, diagnostic) 161 } 162 // "could not import: X" may be an invalid import. 163 case strings.HasPrefix(diagnostic.Message, "could not import: "): 164 ident := strings.TrimPrefix(diagnostic.Message, "could not import: ") 165 if ident == fix.IdentName { 166 results = append(results, diagnostic) 167 } 168 // "X imported but not used" is an unused import. 169 // "X imported but not used as Y" is an unused import. 170 case strings.Contains(diagnostic.Message, " imported but not used"): 171 idx := strings.Index(diagnostic.Message, " imported but not used") 172 importPath := diagnostic.Message[:idx] 173 if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) { 174 results = append(results, diagnostic) 175 } 176 } 177 } 178 return results 179 } 180 181 // getExtractCodeActions returns any refactor.extract code actions for the selection. 182 func getExtractCodeActions(pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { 183 if rng.Start == rng.End { 184 return nil, nil 185 } 186 187 start, end, err := pgf.RangePos(rng) 188 if err != nil { 189 return nil, err 190 } 191 puri := pgf.URI 192 var commands []protocol.Command 193 if _, ok, methodOk, _ := CanExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { 194 cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ 195 Fix: fixExtractFunction, 196 URI: puri, 197 Range: rng, 198 ResolveEdits: supportsResolveEdits(options), 199 }) 200 if err != nil { 201 return nil, err 202 } 203 commands = append(commands, cmd) 204 if methodOk { 205 cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ 206 Fix: fixExtractMethod, 207 URI: puri, 208 Range: rng, 209 ResolveEdits: supportsResolveEdits(options), 210 }) 211 if err != nil { 212 return nil, err 213 } 214 commands = append(commands, cmd) 215 } 216 } 217 if _, _, ok, _ := CanExtractVariable(start, end, pgf.File); ok { 218 cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ 219 Fix: fixExtractVariable, 220 URI: puri, 221 Range: rng, 222 ResolveEdits: supportsResolveEdits(options), 223 }) 224 if err != nil { 225 return nil, err 226 } 227 commands = append(commands, cmd) 228 } 229 var actions []protocol.CodeAction 230 for i := range commands { 231 actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options)) 232 } 233 return actions, nil 234 } 235 236 func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Command, diagnostics []protocol.Diagnostic, options *settings.Options) protocol.CodeAction { 237 action := protocol.CodeAction{ 238 Title: title, 239 Kind: kind, 240 Diagnostics: diagnostics, 241 } 242 if !supportsResolveEdits(options) { 243 action.Command = cmd 244 } else { 245 data, err := json.Marshal(cmd) 246 if err != nil { 247 panic("unable to marshal") 248 } 249 msg := json.RawMessage(data) 250 action.Data = &msg 251 } 252 return action 253 } 254 255 // getRewriteCodeActions returns refactor.rewrite code actions available at the specified range. 256 func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { 257 // golang/go#61693: code actions were refactored to run outside of the 258 // analysis framework, but as a result they lost their panic recovery. 259 // 260 // These code actions should never fail, but put back the panic recovery as a 261 // defensive measure. 262 defer func() { 263 if r := recover(); r != nil { 264 rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r) 265 } 266 }() 267 268 var actions []protocol.CodeAction 269 270 if canRemoveParameter(pkg, pgf, rng) { 271 cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{ 272 RemoveParameter: protocol.Location{ 273 URI: pgf.URI, 274 Range: rng, 275 }, 276 ResolveEdits: supportsResolveEdits(options), 277 }) 278 if err != nil { 279 return nil, err 280 } 281 actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options)) 282 } 283 284 if action, ok := ConvertStringLiteral(pgf, fh, rng); ok { 285 actions = append(actions, action) 286 } 287 288 start, end, err := pgf.RangePos(rng) 289 if err != nil { 290 return nil, err 291 } 292 293 var commands []protocol.Command 294 if _, ok, _ := CanInvertIfCondition(pgf.File, start, end); ok { 295 cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ 296 Fix: fixInvertIfCondition, 297 URI: pgf.URI, 298 Range: rng, 299 ResolveEdits: supportsResolveEdits(options), 300 }) 301 if err != nil { 302 return nil, err 303 } 304 commands = append(commands, cmd) 305 } 306 307 // N.B.: an inspector only pays for itself after ~5 passes, which means we're 308 // currently not getting a good deal on this inspection. 309 // 310 // TODO: Consider removing the inspection after convenienceAnalyzers are removed. 311 inspect := inspector.New([]*ast.File{pgf.File}) 312 for _, diag := range fillstruct.Diagnose(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) { 313 rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End) 314 if err != nil { 315 return nil, err 316 } 317 for _, fix := range diag.SuggestedFixes { 318 cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ 319 Fix: diag.Category, 320 URI: pgf.URI, 321 Range: rng, 322 ResolveEdits: supportsResolveEdits(options), 323 }) 324 if err != nil { 325 return nil, err 326 } 327 commands = append(commands, cmd) 328 } 329 } 330 331 for i := range commands { 332 actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options)) 333 } 334 335 return actions, nil 336 } 337 338 // canRemoveParameter reports whether we can remove the function parameter 339 // indicated by the given [start, end) range. 340 // 341 // This is true if: 342 // - [start, end) is contained within an unused field or parameter name 343 // - ... of a non-method function declaration. 344 // 345 // (Note that the unusedparam analyzer also computes this property, but 346 // much more precisely, allowing it to report its findings as diagnostics.) 347 func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) bool { 348 info, err := FindParam(pgf, rng) 349 if err != nil { 350 return false // e.g. invalid range 351 } 352 if info.Field == nil { 353 return false // range does not span a parameter 354 } 355 if info.Decl.Body == nil { 356 return false // external function 357 } 358 if len(info.Field.Names) == 0 { 359 return true // no names => field is unused 360 } 361 if info.Name == nil { 362 return false // no name is indicated 363 } 364 if info.Name.Name == "_" { 365 return true // trivially unused 366 } 367 368 obj := pkg.GetTypesInfo().Defs[info.Name] 369 if obj == nil { 370 return false // something went wrong 371 } 372 373 used := false 374 ast.Inspect(info.Decl.Body, func(node ast.Node) bool { 375 if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj { 376 used = true 377 } 378 return !used // keep going until we find a use 379 }) 380 return !used 381 } 382 383 // getInlineCodeActions returns refactor.inline actions available at the specified range. 384 func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { 385 start, end, err := pgf.RangePos(rng) 386 if err != nil { 387 return nil, err 388 } 389 390 // If range is within call expression, offer inline action. 391 var commands []protocol.Command 392 if _, fn, err := EnclosingStaticCall(pkg, pgf, start, end); err == nil { 393 cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ 394 Fix: fixInlineCall, 395 URI: pgf.URI, 396 Range: rng, 397 ResolveEdits: supportsResolveEdits(options), 398 }) 399 if err != nil { 400 return nil, err 401 } 402 commands = append(commands, cmd) 403 } 404 405 // Convert commands to actions. 406 var actions []protocol.CodeAction 407 for i := range commands { 408 actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorInline, &commands[i], nil, options)) 409 } 410 return actions, nil 411 } 412 413 // getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection. 414 func getGoTestCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) { 415 fns, err := TestsAndBenchmarks(pkg, pgf) 416 if err != nil { 417 return nil, err 418 } 419 420 var tests, benchmarks []string 421 for _, fn := range fns.Tests { 422 if !protocol.Intersect(fn.Rng, rng) { 423 continue 424 } 425 tests = append(tests, fn.Name) 426 } 427 for _, fn := range fns.Benchmarks { 428 if !protocol.Intersect(fn.Rng, rng) { 429 continue 430 } 431 benchmarks = append(benchmarks, fn.Name) 432 } 433 434 if len(tests) == 0 && len(benchmarks) == 0 { 435 return nil, nil 436 } 437 438 cmd, err := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks) 439 if err != nil { 440 return nil, err 441 } 442 return []protocol.CodeAction{{ 443 Title: cmd.Title, 444 Kind: protocol.GoTest, 445 Command: &cmd, 446 }}, nil 447 } 448 449 func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges { 450 return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits) 451 }