1// Copyright 2019 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 5package cmd 6 7import ( 8 "context" 9 "flag" 10 "fmt" 11 "io/ioutil" 12 "time" 13 14 "golang.org/x/tools/internal/lsp/diff" 15 "golang.org/x/tools/internal/lsp/protocol" 16 "golang.org/x/tools/internal/lsp/source" 17 "golang.org/x/tools/internal/span" 18 "golang.org/x/tools/internal/tool" 19 errors "golang.org/x/xerrors" 20) 21 22// suggestedfix implements the fix verb for gopls. 23type suggestedfix struct { 24 Diff bool `flag:"d" help:"display diffs instead of rewriting files"` 25 Write bool `flag:"w" help:"write result to (source) file instead of stdout"` 26 All bool `flag:"a" help:"apply all fixes, not just preferred fixes"` 27 28 app *Application 29} 30 31func (s *suggestedfix) Name() string { return "fix" } 32func (s *suggestedfix) Usage() string { return "<filename>" } 33func (s *suggestedfix) ShortHelp() string { return "apply suggested fixes" } 34func (s *suggestedfix) DetailedHelp(f *flag.FlagSet) { 35 fmt.Fprintf(f.Output(), ` 36Example: apply suggested fixes for this file: 37 38 $ gopls fix -w internal/lsp/cmd/check.go 39 40gopls fix flags are: 41`) 42 f.PrintDefaults() 43} 44 45// Run performs diagnostic checks on the file specified and either; 46// - if -w is specified, updates the file in place; 47// - if -d is specified, prints out unified diffs of the changes; or 48// - otherwise, prints the new versions to stdout. 49func (s *suggestedfix) Run(ctx context.Context, args ...string) error { 50 if len(args) != 1 { 51 return tool.CommandLineErrorf("fix expects 1 argument") 52 } 53 conn, err := s.app.connect(ctx) 54 if err != nil { 55 return err 56 } 57 defer conn.terminate(ctx) 58 59 from := span.Parse(args[0]) 60 uri := from.URI() 61 file := conn.AddFile(ctx, uri) 62 if file.err != nil { 63 return file.err 64 } 65 66 // Wait for diagnostics results 67 select { 68 case <-file.hasDiagnostics: 69 case <-time.After(30 * time.Second): 70 return errors.Errorf("timed out waiting for results from %v", file.uri) 71 } 72 73 file.diagnosticsMu.Lock() 74 defer file.diagnosticsMu.Unlock() 75 76 p := protocol.CodeActionParams{ 77 TextDocument: protocol.TextDocumentIdentifier{ 78 URI: protocol.NewURI(uri), 79 }, 80 Context: protocol.CodeActionContext{ 81 Only: []protocol.CodeActionKind{protocol.QuickFix}, 82 Diagnostics: file.diagnostics, 83 }, 84 } 85 actions, err := conn.CodeAction(ctx, &p) 86 if err != nil { 87 return errors.Errorf("%v: %v", from, err) 88 } 89 var edits []protocol.TextEdit 90 for _, a := range actions { 91 if !a.IsPreferred && !s.All { 92 continue 93 } 94 for _, c := range a.Edit.DocumentChanges { 95 if c.TextDocument.URI == string(uri) { 96 edits = append(edits, c.Edits...) 97 } 98 } 99 } 100 101 sedits, err := source.FromProtocolEdits(file.mapper, edits) 102 if err != nil { 103 return errors.Errorf("%v: %v", edits, err) 104 } 105 newContent := diff.ApplyEdits(string(file.mapper.Content), sedits) 106 107 filename := file.uri.Filename() 108 switch { 109 case s.Write: 110 if len(edits) > 0 { 111 ioutil.WriteFile(filename, []byte(newContent), 0644) 112 } 113 case s.Diff: 114 diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) 115 fmt.Print(diffs) 116 default: 117 fmt.Print(string(newContent)) 118 } 119 return nil 120} 121