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.
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/internal/event"
16 "golang.org/x/tools/internal/imports"
17 "golang.org/x/tools/internal/lsp/debug/tag"
18 "golang.org/x/tools/internal/lsp/mod"
19 "golang.org/x/tools/internal/lsp/protocol"
20 "golang.org/x/tools/internal/lsp/source"
21 "golang.org/x/tools/internal/span"
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)
32 // Determine the supported actions for this file kind.
33 supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Kind()]
35 return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Kind())
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
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,
49 if len(params.Context.Only) == 0 {
50 wanted = supportedCodeActions
52 wanted = make(map[protocol.CodeActionKind]bool)
53 for _, only := range params.Context.Only {
54 wanted[only] = supportedCodeActions[only] || explicit[only]
58 return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only)
61 var codeActions []protocol.CodeAction
64 if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
65 modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
66 if source.IsNonFatalGoModError(err) {
72 codeActions = append(codeActions, modQuickFixes...)
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) {
80 diagnostics := params.Context.Diagnostics
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)
87 event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Filename()))
89 // Separate this into a set of codeActions per diagnostic, where
90 // each action is the addition, removal, or renaming of one import.
92 for _, importFix := range importEditsPerFix {
93 fixes := importDiagnostics(importFix.Fix, diagnostics)
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),
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)
115 codeActions = append(codeActions, actions...)
117 // Send all of the import edits as one code action if the file is
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),
129 if ctx.Err() != nil {
130 return nil, ctx.Err()
132 pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage)
136 if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
137 analysisQuickFixes, highConfidenceEdits, err := analysisFixes(ctx, snapshot, pkg, diagnostics)
141 if wanted[protocol.QuickFix] {
142 // Add the quick fixes reported by go/analysis.
143 codeActions = append(codeActions, analysisQuickFixes...)
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()))
152 codeActions = append(codeActions, modQuickFixes...)
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,
164 if ctx.Err() != nil {
165 return nil, ctx.Err()
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)
173 codeActions = append(codeActions, fixes...)
175 if wanted[protocol.RefactorExtract] {
176 fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range)
180 codeActions = append(codeActions, fixes...)
183 if wanted[protocol.GoTest] {
184 fixes, err := goTest(ctx, snapshot, uri, params.Range)
188 codeActions = append(codeActions, fixes...)
192 // Unsupported file kind for a code action.
195 return codeActions, nil
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{}{}
205 var result []protocol.CodeActionKind
206 for kind := range allCodeActionKinds {
207 result = append(result, kind)
209 sort.Slice(result, func(i, j int) bool {
210 return result[i] < result[j]
215 func importFixTitle(fix *imports.ImportFix) string {
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)
228 func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) (results []protocol.Diagnostic) {
229 for _, diagnostic := range diagnostics {
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)
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)
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)
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 {
261 codeActions []protocol.CodeAction
262 sourceFixAllEdits []protocol.TextDocumentEdit
264 for _, diag := range diagnostics {
265 srcErr, analyzer, ok := findSourceError(ctx, snapshot, pkg.ID(), diag)
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)
276 codeActions = append(codeActions, *action)
279 for _, fix := range srcErr.SuggestedFixes {
280 action := protocol.CodeAction{
282 Kind: protocol.QuickFix,
283 Diagnostics: []protocol.Diagnostic{diag},
284 Edit: protocol.WorkspaceEdit{},
286 for uri, edits := range fix.Edits {
287 fh, err := snapshot.GetVersionedFile(ctx, uri)
291 docChanges := documentChanges(fh, edits)
292 if analyzer.HighConfidence {
293 sourceFixAllEdits = append(sourceFixAllEdits, docChanges...)
295 action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, docChanges...)
297 codeActions = append(codeActions, action)
300 return codeActions, sourceFixAllEdits, nil
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)
306 return nil, source.Analyzer{}, false
308 analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer)
310 return nil, source.Analyzer{}, false
312 for _, err := range analysisErrors {
313 if err.Message != diag.Message {
316 if protocol.CompareRange(err.Range, diag.Range) != 0 {
319 if err.Category != analyzer.Analyzer.Name {
322 // The error matches.
323 return err, *analyzer, true
325 return nil, source.Analyzer{}, false
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.
334 if analyzer != nil && !analyzer.IsEnabled(snapshot.View()) {
338 if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok {
341 if a, ok := snapshot.View().Options().StaticcheckAnalyzers[src]; ok {
344 if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok {
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 {
352 if src != "compiler" {
355 for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
356 if a.FixesError(msg) {
363 var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
365 func goGetFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
366 if snapshot.GoModForFile(uri) == "" {
367 // Go get only supports module mode for now.
371 var actions []protocol.CodeAction
372 for _, diag := range diagnostics {
373 matches := importErrorRe.FindStringSubmatch(diag.Message)
374 if len(matches) == 0 {
377 args, err := source.MarshalArgs(uri, matches[1])
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(),
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()) {
401 if a.Command == nil {
402 event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
405 analyzers = append(analyzers, a.Analyzer)
407 diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
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.
419 if !protocol.Intersect(d.Range, rng) {
422 action, err := diagnosticToCommandCodeAction(ctx, snapshot, d, nil, protocol.RefactorRewrite)
426 codeActions = append(codeActions, *action)
428 return codeActions, nil
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)
436 return nil, fmt.Errorf("no convenience analyzer for category %s", e.Category)
438 if analyzer.Command == nil {
439 return nil, fmt.Errorf("no command for convenience analyzer %s", analyzer.Analyzer.Name)
441 jsonArgs, err := source.MarshalArgs(e.URI, e.Range)
445 var diagnostics []protocol.Diagnostic
447 diagnostics = append(diagnostics, *d)
449 return &protocol.CodeAction{
452 Diagnostics: diagnostics,
453 Command: &protocol.Command{
454 Command: analyzer.Command.ID(),
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 {
465 fh, err := snapshot.GetFile(ctx, uri)
469 jsonArgs, err := source.MarshalArgs(uri, rng)
473 var actions []protocol.CodeAction
474 for _, command := range []*source.Command{
475 source.CommandExtractFunction,
476 source.CommandExtractVariable,
478 if !command.Applies(ctx, snapshot, fh, rng) {
481 actions = append(actions, protocol.CodeAction{
482 Title: command.Title,
483 Kind: protocol.RefactorExtract,
484 Command: &protocol.Command{
485 Command: command.ID(),
493 func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit {
494 return []protocol.TextDocumentEdit{
496 TextDocument: protocol.VersionedTextDocumentIdentifier{
497 Version: fh.Version(),
498 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
499 URI: protocol.URIFromSpanURI(fh.URI()),
507 func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
508 var modFH source.VersionedFileHandle
513 modURI := snapshot.GoModForFile(fh.URI())
518 modFH, err = snapshot.GetVersionedFile(ctx, modURI)
523 errors, err := mod.ErrorsForMod(ctx, snapshot, modFH)
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) {
539 for _, fix := range e.SuggestedFixes {
540 action := protocol.CodeAction{
542 Kind: protocol.QuickFix,
543 Diagnostics: []protocol.Diagnostic{*diag},
544 Edit: protocol.WorkspaceEdit{},
545 Command: fix.Command,
547 for uri, edits := range fix.Edits {
548 if uri != modFH.URI() {
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()),
561 quickFixes = append(quickFixes, action)
564 return quickFixes, nil
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
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)
576 fns, err := source.TestsAndBenchmarks(ctx, snapshot, fh)
581 var tests, benchmarks []string
582 for _, fn := range fns.Tests {
583 if !protocol.Intersect(fn.Rng, rng) {
586 tests = append(tests, fn.Name)
588 for _, fn := range fns.Benchmarks {
589 if !protocol.Intersect(fn.Rng, rng) {
592 benchmarks = append(benchmarks, fn.Name)
595 if len(tests) == 0 && len(benchmarks) == 0 {
599 jsonArgs, err := source.MarshalArgs(uri, tests, benchmarks)
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(),