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.
13 "golang.org/x/tools/go/analysis"
14 "golang.org/x/tools/internal/event"
15 "golang.org/x/tools/internal/imports"
16 "golang.org/x/tools/internal/lsp/debug/tag"
17 "golang.org/x/tools/internal/lsp/protocol"
18 "golang.org/x/tools/internal/lsp/source"
19 "golang.org/x/tools/internal/span"
22 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
23 snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
30 // Determine the supported actions for this file kind.
31 supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Kind()]
33 return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Kind())
36 // The Only field of the context specifies which code actions the client wants.
37 // If Only is empty, assume that the client wants all of the non-explicit code actions.
38 var wanted map[protocol.CodeActionKind]bool
40 // Explicit Code Actions are opt-in and shouldn't be returned to the client unless
41 // requested using Only.
42 // TODO: Add other CodeLenses such as GoGenerate, RegenerateCgo, etc..
43 explicit := map[protocol.CodeActionKind]bool{
44 protocol.GoTest: true,
47 if len(params.Context.Only) == 0 {
48 wanted = supportedCodeActions
50 wanted = make(map[protocol.CodeActionKind]bool)
51 for _, only := range params.Context.Only {
52 wanted[only] = supportedCodeActions[only] || explicit[only]
56 return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only)
59 var codeActions []protocol.CodeAction
62 if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
63 modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
64 if source.IsNonFatalGoModError(err) {
70 codeActions = append(codeActions, modQuickFixes...)
72 if wanted[protocol.SourceOrganizeImports] {
73 action, err := goModTidy(ctx, snapshot, fh)
74 if source.IsNonFatalGoModError(err) {
80 codeActions = append(codeActions, *action)
83 // Don't suggest fixes for generated files, since they are generally
84 // not useful and some editors may apply them automatically on save.
85 if source.IsGenerated(ctx, snapshot, uri) {
88 diagnostics := params.Context.Diagnostics
90 // First, process any missing imports and pair them with the
91 // diagnostics they fix.
92 if wantQuickFixes := wanted[protocol.QuickFix] && len(diagnostics) > 0; wantQuickFixes || wanted[protocol.SourceOrganizeImports] {
93 importEdits, importEditsPerFix, err := source.AllImportsFixes(ctx, snapshot, fh)
95 event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Filename()))
97 // Separate this into a set of codeActions per diagnostic, where
98 // each action is the addition, removal, or renaming of one import.
100 for _, importFix := range importEditsPerFix {
101 fixes := importDiagnostics(importFix.Fix, diagnostics)
105 codeActions = append(codeActions, protocol.CodeAction{
106 Title: importFixTitle(importFix.Fix),
107 Kind: protocol.QuickFix,
108 Edit: protocol.WorkspaceEdit{
109 DocumentChanges: documentChanges(fh, importFix.Edits),
115 // Send all of the import edits as one code action if the file is
117 if wanted[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
118 codeActions = append(codeActions, protocol.CodeAction{
119 Title: "Organize Imports",
120 Kind: protocol.SourceOrganizeImports,
121 Edit: protocol.WorkspaceEdit{
122 DocumentChanges: documentChanges(fh, importEdits),
127 if ctx.Err() != nil {
128 return nil, ctx.Err()
130 pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage)
134 if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
135 analysisQuickFixes, highConfidenceEdits, err := analysisFixes(ctx, snapshot, pkg, diagnostics)
139 if wanted[protocol.QuickFix] {
140 // Add the quick fixes reported by go/analysis.
141 codeActions = append(codeActions, analysisQuickFixes...)
143 // If there are any diagnostics relating to the go.mod file,
144 // add their corresponding quick fixes.
145 modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
146 if source.IsNonFatalGoModError(err) {
147 // Not a fatal error.
148 event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder()))
150 codeActions = append(codeActions, modQuickFixes...)
152 if wanted[protocol.SourceFixAll] && len(highConfidenceEdits) > 0 {
153 codeActions = append(codeActions, protocol.CodeAction{
154 Title: "Simplifications",
155 Kind: protocol.SourceFixAll,
156 Edit: protocol.WorkspaceEdit{
157 DocumentChanges: highConfidenceEdits,
162 if ctx.Err() != nil {
163 return nil, ctx.Err()
165 // Add any suggestions that do not necessarily fix any diagnostics.
166 if wanted[protocol.RefactorRewrite] {
167 fixes, err := convenienceFixes(ctx, snapshot, pkg, uri, params.Range)
171 codeActions = append(codeActions, fixes...)
173 if wanted[protocol.RefactorExtract] {
174 fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range)
178 codeActions = append(codeActions, fixes...)
181 if wanted[protocol.GoTest] {
182 fixes, err := goTest(ctx, snapshot, uri, params.Range)
186 codeActions = append(codeActions, fixes...)
190 // Unsupported file kind for a code action.
193 return codeActions, nil
196 func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
197 allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
198 for _, kinds := range s.session.Options().SupportedCodeActions {
199 for kind := range kinds {
200 allCodeActionKinds[kind] = struct{}{}
203 var result []protocol.CodeActionKind
204 for kind := range allCodeActionKinds {
205 result = append(result, kind)
207 sort.Slice(result, func(i, j int) bool {
208 return result[i] < result[j]
213 func importFixTitle(fix *imports.ImportFix) string {
216 case imports.AddImport:
217 str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
218 case imports.DeleteImport:
219 str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
220 case imports.SetImportName:
221 str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
226 func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) (results []protocol.Diagnostic) {
227 for _, diagnostic := range diagnostics {
229 // "undeclared name: X" may be an unresolved import.
230 case strings.HasPrefix(diagnostic.Message, "undeclared name: "):
231 ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ")
232 if ident == fix.IdentName {
233 results = append(results, diagnostic)
235 // "could not import: X" may be an invalid import.
236 case strings.HasPrefix(diagnostic.Message, "could not import: "):
237 ident := strings.TrimPrefix(diagnostic.Message, "could not import: ")
238 if ident == fix.IdentName {
239 results = append(results, diagnostic)
241 // "X imported but not used" is an unused import.
242 // "X imported but not used as Y" is an unused import.
243 case strings.Contains(diagnostic.Message, " imported but not used"):
244 idx := strings.Index(diagnostic.Message, " imported but not used")
245 importPath := diagnostic.Message[:idx]
246 if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) {
247 results = append(results, diagnostic)
254 func analysisFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, []protocol.TextDocumentEdit, error) {
255 if len(diagnostics) == 0 {
259 codeActions []protocol.CodeAction
260 sourceFixAllEdits []protocol.TextDocumentEdit
262 for _, diag := range diagnostics {
263 srcErr, analyzer, ok := findSourceError(ctx, snapshot, pkg.ID(), diag)
267 // If the suggested fix for the diagnostic is expected to be separate,
268 // see if there are any supported commands available.
269 if analyzer.Command != nil {
270 action, err := diagnosticToCommandCodeAction(ctx, snapshot, srcErr, &diag, protocol.QuickFix)
274 codeActions = append(codeActions, *action)
277 for _, fix := range srcErr.SuggestedFixes {
278 action := protocol.CodeAction{
280 Kind: protocol.QuickFix,
281 Diagnostics: []protocol.Diagnostic{diag},
282 Edit: protocol.WorkspaceEdit{},
284 for uri, edits := range fix.Edits {
285 fh, err := snapshot.GetVersionedFile(ctx, uri)
289 docChanges := documentChanges(fh, edits)
290 if analyzer.HighConfidence {
291 sourceFixAllEdits = append(sourceFixAllEdits, docChanges...)
293 action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, docChanges...)
295 codeActions = append(codeActions, action)
298 return codeActions, sourceFixAllEdits, nil
301 func findSourceError(ctx context.Context, snapshot source.Snapshot, pkgID string, diag protocol.Diagnostic) (*source.Error, source.Analyzer, bool) {
302 analyzer := diagnosticToAnalyzer(snapshot, diag.Source, diag.Message)
304 return nil, source.Analyzer{}, false
306 analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer)
308 return nil, source.Analyzer{}, false
310 for _, err := range analysisErrors {
311 if err.Message != diag.Message {
314 if protocol.CompareRange(err.Range, diag.Range) != 0 {
317 if err.Category != analyzer.Analyzer.Name {
320 // The error matches.
321 return err, *analyzer, true
323 return nil, source.Analyzer{}, false
326 // diagnosticToAnalyzer return the analyzer associated with a given diagnostic.
327 // It assumes that the diagnostic's source will be the name of the analyzer.
328 // If this changes, this approach will need to be reworked.
329 func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *source.Analyzer) {
330 // Make sure that the analyzer we found is enabled.
332 if analyzer != nil && !analyzer.IsEnabled(snapshot.View()) {
336 if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok {
339 if a, ok := snapshot.View().Options().StaticcheckAnalyzers[src]; ok {
342 if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok {
345 // Hack: We publish diagnostics with the source "compiler" for type errors,
346 // but these analyzers have different names. Try both possibilities.
347 if a, ok := snapshot.View().Options().TypeErrorAnalyzers[src]; ok {
350 if src != "compiler" {
353 for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
354 if a.FixesError(msg) {
361 func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
362 var analyzers []*analysis.Analyzer
363 for _, a := range snapshot.View().Options().ConvenienceAnalyzers {
364 if !a.IsEnabled(snapshot.View()) {
367 if a.Command == nil {
368 event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
371 analyzers = append(analyzers, a.Analyzer)
373 diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
377 var codeActions []protocol.CodeAction
378 for _, d := range diagnostics {
379 // For now, only show diagnostics for matching lines. Maybe we should
380 // alter this behavior in the future, depending on the user experience.
385 if !protocol.Intersect(d.Range, rng) {
388 action, err := diagnosticToCommandCodeAction(ctx, snapshot, d, nil, protocol.RefactorRewrite)
392 codeActions = append(codeActions, *action)
394 return codeActions, nil
397 func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot, e *source.Error, d *protocol.Diagnostic, kind protocol.CodeActionKind) (*protocol.CodeAction, error) {
398 // The fix depends on the category of the analyzer. The diagnostic may be
399 // nil, so use the error's category.
400 analyzer := diagnosticToAnalyzer(snapshot, e.Category, e.Message)
402 return nil, fmt.Errorf("no convenience analyzer for category %s", e.Category)
404 if analyzer.Command == nil {
405 return nil, fmt.Errorf("no command for convenience analyzer %s", analyzer.Analyzer.Name)
407 jsonArgs, err := source.MarshalArgs(e.URI, e.Range)
411 var diagnostics []protocol.Diagnostic
413 diagnostics = append(diagnostics, *d)
415 return &protocol.CodeAction{
418 Diagnostics: diagnostics,
419 Command: &protocol.Command{
420 Command: analyzer.Command.ID(),
427 func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
428 if rng.Start == rng.End {
431 fh, err := snapshot.GetFile(ctx, uri)
435 jsonArgs, err := source.MarshalArgs(uri, rng)
439 var actions []protocol.CodeAction
440 for _, command := range []*source.Command{
441 source.CommandExtractFunction,
442 source.CommandExtractVariable,
444 if !command.Applies(ctx, snapshot, fh, rng) {
447 actions = append(actions, protocol.CodeAction{
448 Title: command.Title,
449 Kind: protocol.RefactorExtract,
450 Command: &protocol.Command{
451 Command: command.ID(),
459 func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit {
460 return []protocol.TextDocumentEdit{
462 TextDocument: protocol.VersionedTextDocumentIdentifier{
463 Version: fh.Version(),
464 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
465 URI: protocol.URIFromSpanURI(fh.URI()),
473 func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
474 var modFH source.VersionedFileHandle
479 modURI := snapshot.GoModForFile(ctx, fh.URI())
484 modFH, err = snapshot.GetVersionedFile(ctx, modURI)
489 tidied, err := snapshot.ModTidy(ctx, modFH)
493 var quickFixes []protocol.CodeAction
494 for _, e := range tidied.Errors {
495 var diag *protocol.Diagnostic
496 for _, d := range diagnostics {
497 if sameDiagnostic(d, e) {
505 for _, fix := range e.SuggestedFixes {
506 action := protocol.CodeAction{
508 Kind: protocol.QuickFix,
509 Diagnostics: []protocol.Diagnostic{*diag},
510 Edit: protocol.WorkspaceEdit{},
512 for uri, edits := range fix.Edits {
513 if uri != modFH.URI() {
516 action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
517 TextDocument: protocol.VersionedTextDocumentIdentifier{
518 Version: modFH.Version(),
519 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
520 URI: protocol.URIFromSpanURI(modFH.URI()),
526 quickFixes = append(quickFixes, action)
529 return quickFixes, nil
532 func sameDiagnostic(d protocol.Diagnostic, e source.Error) bool {
533 return d.Message == e.Message && protocol.CompareRange(d.Range, e.Range) == 0 && d.Source == e.Category
536 func goModTidy(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle) (*protocol.CodeAction, error) {
537 tidied, err := snapshot.ModTidy(ctx, fh)
541 left, err := fh.Read()
545 right := tidied.TidiedContent
546 edits := snapshot.View().Options().ComputeEdits(fh.URI(), string(left), string(right))
547 protocolEdits, err := source.ToProtocolEdits(tidied.Parsed.Mapper, edits)
551 return &protocol.CodeAction{
553 Kind: protocol.SourceOrganizeImports,
554 Edit: protocol.WorkspaceEdit{
555 DocumentChanges: []protocol.TextDocumentEdit{{
556 TextDocument: protocol.VersionedTextDocumentIdentifier{
557 Version: fh.Version(),
558 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
559 URI: protocol.URIFromSpanURI(fh.URI()),
562 Edits: protocolEdits,
568 func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
569 fh, err := snapshot.GetFile(ctx, uri)
573 fns, err := source.TestsAndBenchmarks(ctx, snapshot, fh)
578 var tests, benchmarks []string
579 for _, fn := range fns.Tests {
580 if !protocol.Intersect(fn.Rng, rng) {
583 tests = append(tests, fn.Name)
585 for _, fn := range fns.Benchmarks {
586 if !protocol.Intersect(fn.Rng, rng) {
589 benchmarks = append(benchmarks, fn.Name)
592 if len(tests) == 0 && len(benchmarks) == 0 {
596 jsonArgs, err := source.MarshalArgs(uri, tests, benchmarks)
600 return []protocol.CodeAction{{
601 Title: source.CommandTest.Name,
602 Kind: protocol.GoTest,
603 Command: &protocol.Command{
604 Title: source.CommandTest.Title,
605 Command: source.CommandTest.ID(),