github.com/jd-ly/tools@v0.5.7/internal/lsp/code_action.go (about) 1 // Copyright 2018 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 lsp 6 7 import ( 8 "context" 9 "fmt" 10 "regexp" 11 "sort" 12 "strings" 13 14 "github.com/jd-ly/tools/go/analysis" 15 "github.com/jd-ly/tools/internal/event" 16 "github.com/jd-ly/tools/internal/imports" 17 "github.com/jd-ly/tools/internal/lsp/debug/tag" 18 "github.com/jd-ly/tools/internal/lsp/mod" 19 "github.com/jd-ly/tools/internal/lsp/protocol" 20 "github.com/jd-ly/tools/internal/lsp/source" 21 "github.com/jd-ly/tools/internal/span" 22 ) 23 24 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { 25 snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) 26 defer release() 27 if !ok { 28 return nil, err 29 } 30 uri := fh.URI() 31 32 // Determine the supported actions for this file kind. 33 supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Kind()] 34 if !ok { 35 return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Kind()) 36 } 37 38 // The Only field of the context specifies which code actions the client wants. 39 // If Only is empty, assume that the client wants all of the non-explicit code actions. 40 var wanted map[protocol.CodeActionKind]bool 41 42 // Explicit Code Actions are opt-in and shouldn't be returned to the client unless 43 // requested using Only. 44 // TODO: Add other CodeLenses such as GoGenerate, RegenerateCgo, etc.. 45 explicit := map[protocol.CodeActionKind]bool{ 46 protocol.GoTest: true, 47 } 48 49 if len(params.Context.Only) == 0 { 50 wanted = supportedCodeActions 51 } else { 52 wanted = make(map[protocol.CodeActionKind]bool) 53 for _, only := range params.Context.Only { 54 wanted[only] = supportedCodeActions[only] || explicit[only] 55 } 56 } 57 if len(wanted) == 0 { 58 return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only) 59 } 60 61 var codeActions []protocol.CodeAction 62 switch fh.Kind() { 63 case source.Mod: 64 if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 { 65 modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics) 66 if source.IsNonFatalGoModError(err) { 67 return nil, nil 68 } 69 if err != nil { 70 return nil, err 71 } 72 codeActions = append(codeActions, modQuickFixes...) 73 } 74 case source.Go: 75 // Don't suggest fixes for generated files, since they are generally 76 // not useful and some editors may apply them automatically on save. 77 if source.IsGenerated(ctx, snapshot, uri) { 78 return nil, nil 79 } 80 diagnostics := params.Context.Diagnostics 81 82 // First, process any missing imports and pair them with the 83 // diagnostics they fix. 84 if wantQuickFixes := wanted[protocol.QuickFix] && len(diagnostics) > 0; wantQuickFixes || wanted[protocol.SourceOrganizeImports] { 85 importEdits, importEditsPerFix, err := source.AllImportsFixes(ctx, snapshot, fh) 86 if err != nil { 87 event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Filename())) 88 } 89 // Separate this into a set of codeActions per diagnostic, where 90 // each action is the addition, removal, or renaming of one import. 91 if wantQuickFixes { 92 for _, importFix := range importEditsPerFix { 93 fixes := importDiagnostics(importFix.Fix, diagnostics) 94 if len(fixes) == 0 { 95 continue 96 } 97 codeActions = append(codeActions, protocol.CodeAction{ 98 Title: importFixTitle(importFix.Fix), 99 Kind: protocol.QuickFix, 100 Edit: protocol.WorkspaceEdit{ 101 DocumentChanges: documentChanges(fh, importFix.Edits), 102 }, 103 Diagnostics: fixes, 104 }) 105 } 106 } 107 108 // Fix unresolved imports with "go get". This is separate from the 109 // goimports fixes because goimports will not remove an import 110 // that appears to be used, even if currently unresolved. 111 actions, err := goGetFixes(ctx, snapshot, fh.URI(), diagnostics) 112 if err != nil { 113 return nil, err 114 } 115 codeActions = append(codeActions, actions...) 116 117 // Send all of the import edits as one code action if the file is 118 // being organized. 119 if wanted[protocol.SourceOrganizeImports] && len(importEdits) > 0 { 120 codeActions = append(codeActions, protocol.CodeAction{ 121 Title: "Organize Imports", 122 Kind: protocol.SourceOrganizeImports, 123 Edit: protocol.WorkspaceEdit{ 124 DocumentChanges: documentChanges(fh, importEdits), 125 }, 126 }) 127 } 128 } 129 if ctx.Err() != nil { 130 return nil, ctx.Err() 131 } 132 pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage) 133 if err != nil { 134 return nil, err 135 } 136 if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 { 137 analysisQuickFixes, highConfidenceEdits, err := analysisFixes(ctx, snapshot, pkg, diagnostics) 138 if err != nil { 139 return nil, err 140 } 141 if wanted[protocol.QuickFix] { 142 // Add the quick fixes reported by go/analysis. 143 codeActions = append(codeActions, analysisQuickFixes...) 144 145 // If there are any diagnostics relating to the go.mod file, 146 // add their corresponding quick fixes. 147 modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics) 148 if source.IsNonFatalGoModError(err) { 149 // Not a fatal error. 150 event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder())) 151 } 152 codeActions = append(codeActions, modQuickFixes...) 153 } 154 if wanted[protocol.SourceFixAll] && len(highConfidenceEdits) > 0 { 155 codeActions = append(codeActions, protocol.CodeAction{ 156 Title: "Simplifications", 157 Kind: protocol.SourceFixAll, 158 Edit: protocol.WorkspaceEdit{ 159 DocumentChanges: highConfidenceEdits, 160 }, 161 }) 162 } 163 } 164 if ctx.Err() != nil { 165 return nil, ctx.Err() 166 } 167 // Add any suggestions that do not necessarily fix any diagnostics. 168 if wanted[protocol.RefactorRewrite] { 169 fixes, err := convenienceFixes(ctx, snapshot, pkg, uri, params.Range) 170 if err != nil { 171 return nil, err 172 } 173 codeActions = append(codeActions, fixes...) 174 } 175 if wanted[protocol.RefactorExtract] { 176 fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range) 177 if err != nil { 178 return nil, err 179 } 180 codeActions = append(codeActions, fixes...) 181 } 182 183 if wanted[protocol.GoTest] { 184 fixes, err := goTest(ctx, snapshot, uri, params.Range) 185 if err != nil { 186 return nil, err 187 } 188 codeActions = append(codeActions, fixes...) 189 } 190 191 default: 192 // Unsupported file kind for a code action. 193 return nil, nil 194 } 195 return codeActions, nil 196 } 197 198 func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind { 199 allCodeActionKinds := make(map[protocol.CodeActionKind]struct{}) 200 for _, kinds := range s.session.Options().SupportedCodeActions { 201 for kind := range kinds { 202 allCodeActionKinds[kind] = struct{}{} 203 } 204 } 205 var result []protocol.CodeActionKind 206 for kind := range allCodeActionKinds { 207 result = append(result, kind) 208 } 209 sort.Slice(result, func(i, j int) bool { 210 return result[i] < result[j] 211 }) 212 return result 213 } 214 215 func importFixTitle(fix *imports.ImportFix) string { 216 var str string 217 switch fix.FixType { 218 case imports.AddImport: 219 str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) 220 case imports.DeleteImport: 221 str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) 222 case imports.SetImportName: 223 str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) 224 } 225 return str 226 } 227 228 func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) (results []protocol.Diagnostic) { 229 for _, diagnostic := range diagnostics { 230 switch { 231 // "undeclared name: X" may be an unresolved import. 232 case strings.HasPrefix(diagnostic.Message, "undeclared name: "): 233 ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ") 234 if ident == fix.IdentName { 235 results = append(results, diagnostic) 236 } 237 // "could not import: X" may be an invalid import. 238 case strings.HasPrefix(diagnostic.Message, "could not import: "): 239 ident := strings.TrimPrefix(diagnostic.Message, "could not import: ") 240 if ident == fix.IdentName { 241 results = append(results, diagnostic) 242 } 243 // "X imported but not used" is an unused import. 244 // "X imported but not used as Y" is an unused import. 245 case strings.Contains(diagnostic.Message, " imported but not used"): 246 idx := strings.Index(diagnostic.Message, " imported but not used") 247 importPath := diagnostic.Message[:idx] 248 if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) { 249 results = append(results, diagnostic) 250 } 251 } 252 } 253 return results 254 } 255 256 func analysisFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, []protocol.TextDocumentEdit, error) { 257 if len(diagnostics) == 0 { 258 return nil, nil, nil 259 } 260 var ( 261 codeActions []protocol.CodeAction 262 sourceFixAllEdits []protocol.TextDocumentEdit 263 ) 264 for _, diag := range diagnostics { 265 srcErr, analyzer, ok := findSourceError(ctx, snapshot, pkg.ID(), diag) 266 if !ok { 267 continue 268 } 269 // If the suggested fix for the diagnostic is expected to be separate, 270 // see if there are any supported commands available. 271 if analyzer.Command != nil { 272 action, err := diagnosticToCommandCodeAction(ctx, snapshot, srcErr, &diag, protocol.QuickFix) 273 if err != nil { 274 return nil, nil, err 275 } 276 codeActions = append(codeActions, *action) 277 continue 278 } 279 for _, fix := range srcErr.SuggestedFixes { 280 action := protocol.CodeAction{ 281 Title: fix.Title, 282 Kind: protocol.QuickFix, 283 Diagnostics: []protocol.Diagnostic{diag}, 284 Edit: protocol.WorkspaceEdit{}, 285 } 286 for uri, edits := range fix.Edits { 287 fh, err := snapshot.GetVersionedFile(ctx, uri) 288 if err != nil { 289 return nil, nil, err 290 } 291 docChanges := documentChanges(fh, edits) 292 if analyzer.HighConfidence { 293 sourceFixAllEdits = append(sourceFixAllEdits, docChanges...) 294 } 295 action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, docChanges...) 296 } 297 codeActions = append(codeActions, action) 298 } 299 } 300 return codeActions, sourceFixAllEdits, nil 301 } 302 303 func findSourceError(ctx context.Context, snapshot source.Snapshot, pkgID string, diag protocol.Diagnostic) (*source.Error, source.Analyzer, bool) { 304 analyzer := diagnosticToAnalyzer(snapshot, diag.Source, diag.Message) 305 if analyzer == nil { 306 return nil, source.Analyzer{}, false 307 } 308 analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer) 309 if err != nil { 310 return nil, source.Analyzer{}, false 311 } 312 for _, err := range analysisErrors { 313 if err.Message != diag.Message { 314 continue 315 } 316 if protocol.CompareRange(err.Range, diag.Range) != 0 { 317 continue 318 } 319 if err.Category != analyzer.Analyzer.Name { 320 continue 321 } 322 // The error matches. 323 return err, *analyzer, true 324 } 325 return nil, source.Analyzer{}, false 326 } 327 328 // diagnosticToAnalyzer return the analyzer associated with a given diagnostic. 329 // It assumes that the diagnostic's source will be the name of the analyzer. 330 // If this changes, this approach will need to be reworked. 331 func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *source.Analyzer) { 332 // Make sure that the analyzer we found is enabled. 333 defer func() { 334 if analyzer != nil && !analyzer.IsEnabled(snapshot.View()) { 335 analyzer = nil 336 } 337 }() 338 if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok { 339 return &a 340 } 341 if a, ok := snapshot.View().Options().StaticcheckAnalyzers[src]; ok { 342 return &a 343 } 344 if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok { 345 return &a 346 } 347 // Hack: We publish diagnostics with the source "compiler" for type errors, 348 // but these analyzers have different names. Try both possibilities. 349 if a, ok := snapshot.View().Options().TypeErrorAnalyzers[src]; ok { 350 return &a 351 } 352 if src != "compiler" { 353 return nil 354 } 355 for _, a := range snapshot.View().Options().TypeErrorAnalyzers { 356 if a.FixesError(msg) { 357 return &a 358 } 359 } 360 return nil 361 } 362 363 var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`) 364 365 func goGetFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { 366 if snapshot.GoModForFile(ctx, uri) == "" { 367 // Go get only supports module mode for now. 368 return nil, nil 369 } 370 371 var actions []protocol.CodeAction 372 for _, diag := range diagnostics { 373 matches := importErrorRe.FindStringSubmatch(diag.Message) 374 if len(matches) == 0 { 375 return nil, nil 376 } 377 args, err := source.MarshalArgs(uri, matches[1]) 378 if err != nil { 379 return nil, err 380 } 381 actions = append(actions, protocol.CodeAction{ 382 Title: fmt.Sprintf("go get package %v", matches[1]), 383 Diagnostics: []protocol.Diagnostic{diag}, 384 Kind: protocol.QuickFix, 385 Command: &protocol.Command{ 386 Title: source.CommandGoGetPackage.Title, 387 Command: source.CommandGoGetPackage.ID(), 388 Arguments: args, 389 }, 390 }) 391 } 392 return actions, nil 393 } 394 395 func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { 396 var analyzers []*analysis.Analyzer 397 for _, a := range snapshot.View().Options().ConvenienceAnalyzers { 398 if !a.IsEnabled(snapshot.View()) { 399 continue 400 } 401 if a.Command == nil { 402 event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name)) 403 continue 404 } 405 analyzers = append(analyzers, a.Analyzer) 406 } 407 diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...) 408 if err != nil { 409 return nil, err 410 } 411 var codeActions []protocol.CodeAction 412 for _, d := range diagnostics { 413 // For now, only show diagnostics for matching lines. Maybe we should 414 // alter this behavior in the future, depending on the user experience. 415 if d.URI != uri { 416 continue 417 } 418 419 if !protocol.Intersect(d.Range, rng) { 420 continue 421 } 422 action, err := diagnosticToCommandCodeAction(ctx, snapshot, d, nil, protocol.RefactorRewrite) 423 if err != nil { 424 return nil, err 425 } 426 codeActions = append(codeActions, *action) 427 } 428 return codeActions, nil 429 } 430 431 func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot, e *source.Error, d *protocol.Diagnostic, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { 432 // The fix depends on the category of the analyzer. The diagnostic may be 433 // nil, so use the error's category. 434 analyzer := diagnosticToAnalyzer(snapshot, e.Category, e.Message) 435 if analyzer == nil { 436 return nil, fmt.Errorf("no convenience analyzer for category %s", e.Category) 437 } 438 if analyzer.Command == nil { 439 return nil, fmt.Errorf("no command for convenience analyzer %s", analyzer.Analyzer.Name) 440 } 441 jsonArgs, err := source.MarshalArgs(e.URI, e.Range) 442 if err != nil { 443 return nil, err 444 } 445 var diagnostics []protocol.Diagnostic 446 if d != nil { 447 diagnostics = append(diagnostics, *d) 448 } 449 return &protocol.CodeAction{ 450 Title: e.Message, 451 Kind: kind, 452 Diagnostics: diagnostics, 453 Command: &protocol.Command{ 454 Command: analyzer.Command.ID(), 455 Title: e.Message, 456 Arguments: jsonArgs, 457 }, 458 }, nil 459 } 460 461 func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { 462 if rng.Start == rng.End { 463 return nil, nil 464 } 465 fh, err := snapshot.GetFile(ctx, uri) 466 if err != nil { 467 return nil, err 468 } 469 jsonArgs, err := source.MarshalArgs(uri, rng) 470 if err != nil { 471 return nil, err 472 } 473 var actions []protocol.CodeAction 474 for _, command := range []*source.Command{ 475 source.CommandExtractFunction, 476 source.CommandExtractVariable, 477 } { 478 if !command.Applies(ctx, snapshot, fh, rng) { 479 continue 480 } 481 actions = append(actions, protocol.CodeAction{ 482 Title: command.Title, 483 Kind: protocol.RefactorExtract, 484 Command: &protocol.Command{ 485 Command: command.ID(), 486 Arguments: jsonArgs, 487 }, 488 }) 489 } 490 return actions, nil 491 } 492 493 func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit { 494 return []protocol.TextDocumentEdit{ 495 { 496 TextDocument: protocol.VersionedTextDocumentIdentifier{ 497 Version: fh.Version(), 498 TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 499 URI: protocol.URIFromSpanURI(fh.URI()), 500 }, 501 }, 502 Edits: edits, 503 }, 504 } 505 } 506 507 func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { 508 var modFH source.VersionedFileHandle 509 switch fh.Kind() { 510 case source.Mod: 511 modFH = fh 512 case source.Go: 513 modURI := snapshot.GoModForFile(ctx, fh.URI()) 514 if modURI == "" { 515 return nil, nil 516 } 517 var err error 518 modFH, err = snapshot.GetVersionedFile(ctx, modURI) 519 if err != nil { 520 return nil, err 521 } 522 } 523 errors, err := mod.ErrorsForMod(ctx, snapshot, modFH) 524 if err != nil { 525 return nil, err 526 } 527 var quickFixes []protocol.CodeAction 528 for _, e := range errors { 529 var diag *protocol.Diagnostic 530 for _, d := range diagnostics { 531 if sameDiagnostic(d, e) { 532 diag = &d 533 break 534 } 535 } 536 if diag == nil { 537 continue 538 } 539 for _, fix := range e.SuggestedFixes { 540 action := protocol.CodeAction{ 541 Title: fix.Title, 542 Kind: protocol.QuickFix, 543 Diagnostics: []protocol.Diagnostic{*diag}, 544 Edit: protocol.WorkspaceEdit{}, 545 Command: fix.Command, 546 } 547 for uri, edits := range fix.Edits { 548 if uri != modFH.URI() { 549 continue 550 } 551 action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{ 552 TextDocument: protocol.VersionedTextDocumentIdentifier{ 553 Version: modFH.Version(), 554 TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 555 URI: protocol.URIFromSpanURI(modFH.URI()), 556 }, 557 }, 558 Edits: edits, 559 }) 560 } 561 quickFixes = append(quickFixes, action) 562 } 563 } 564 return quickFixes, nil 565 } 566 567 func sameDiagnostic(d protocol.Diagnostic, e *source.Error) bool { 568 return d.Message == e.Message && protocol.CompareRange(d.Range, e.Range) == 0 && d.Source == e.Category 569 } 570 571 func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { 572 fh, err := snapshot.GetFile(ctx, uri) 573 if err != nil { 574 return nil, err 575 } 576 fns, err := source.TestsAndBenchmarks(ctx, snapshot, fh) 577 if err != nil { 578 return nil, err 579 } 580 581 var tests, benchmarks []string 582 for _, fn := range fns.Tests { 583 if !protocol.Intersect(fn.Rng, rng) { 584 continue 585 } 586 tests = append(tests, fn.Name) 587 } 588 for _, fn := range fns.Benchmarks { 589 if !protocol.Intersect(fn.Rng, rng) { 590 continue 591 } 592 benchmarks = append(benchmarks, fn.Name) 593 } 594 595 if len(tests) == 0 && len(benchmarks) == 0 { 596 return nil, nil 597 } 598 599 jsonArgs, err := source.MarshalArgs(uri, tests, benchmarks) 600 if err != nil { 601 return nil, err 602 } 603 return []protocol.CodeAction{{ 604 Title: source.CommandTest.Name, 605 Kind: protocol.GoTest, 606 Command: &protocol.Command{ 607 Title: source.CommandTest.Title, 608 Command: source.CommandTest.ID(), 609 Arguments: jsonArgs, 610 }, 611 }}, nil 612 }