Skip to content

tools/gopls: add command line support for suggestedfix #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/lsp/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (app *Application) commands() []tool.Application {
&format{app: app},
&query{app: app},
&rename{app: app},
&suggestedfix{app: app},
&version{app: app},
}
}
Expand Down
115 changes: 115 additions & 0 deletions internal/lsp/cmd/suggested_fix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"context"
"flag"
"fmt"
"io/ioutil"
"time"

"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/tool"
errors "golang.org/x/xerrors"
)

// suggestedfix implements the fix verb for gopls.
type suggestedfix struct {
Diff bool `flag:"d" help:"display diffs instead of rewriting files"`
Write bool `flag:"w" help:"write result to (source) file instead of stdout"`
All bool `flag:"a" help:"apply all fixes, not just preferred fixes"`

app *Application
}

func (s *suggestedfix) Name() string { return "fix" }
func (s *suggestedfix) Usage() string { return "<filename>" }
func (s *suggestedfix) ShortHelp() string { return "apply suggested fixes" }
func (s *suggestedfix) DetailedHelp(f *flag.FlagSet) {
fmt.Fprintf(f.Output(), `
Example: apply suggested fixes for this file:

  $ gopls fix -w internal/lsp/cmd/check.go

gopls fix flags are:
`)
f.PrintDefaults()
}

// Run performs diagnostic checks on the file specified and either;
// - if -w is specified, updates the file in place;
// - if -d is specified, prints out unified diffs of the changes; or
// - otherwise, prints the new versions to stdout.
func (s *suggestedfix) Run(ctx context.Context, args ...string) error {
if len(args) != 1 {
return tool.CommandLineErrorf("fix expects 1 argument")
}
conn, err := s.app.connect(ctx)
if err != nil {
return err
}
defer conn.terminate(ctx)

from := span.Parse(args[0])
uri := from.URI()
file := conn.AddFile(ctx, uri)
if file.err != nil {
return file.err
}

// Wait for diagnostics results
select {
case <-file.hasDiagnostics:
case <-time.After(30 * time.Second):
return errors.Errorf("timed out waiting for results from %v", file.uri)
}

file.diagnosticsMu.Lock()
defer file.diagnosticsMu.Unlock()

p := protocol.CodeActionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(uri),
},
Context: protocol.CodeActionContext{
Only: []protocol.CodeActionKind{protocol.QuickFix},
Diagnostics: file.diagnostics,
},
}
actions, err := conn.CodeAction(ctx, &p)
if err != nil {
return errors.Errorf("%v: %v", from, err)
}
var edits []protocol.TextEdit
for _, a := range actions {
if a.IsPreferred || s.All {
edits = (*a.Edit.Changes)[string(uri)]
}
}

sedits, err := source.FromProtocolEdits(file.mapper, edits)
if err != nil {
return errors.Errorf("%v: %v", edits, err)
}
newContent := diff.ApplyEdits(string(file.mapper.Content), sedits)

filename := file.uri.Filename()
switch {
case s.Write:
if len(edits) > 0 {
ioutil.WriteFile(filename, []byte(newContent), 0644)
}
case s.Diff:
diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits)
fmt.Print(diffs)
default:
fmt.Print(string(newContent))
}
return nil
}
4 changes: 0 additions & 4 deletions internal/lsp/cmd/test/cmdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,6 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
//TODO: add command line imports tests when it works
}

func (r *runner) SuggestedFix(t *testing.T, spn span.Span) {
//TODO: add suggested fix tests when it works
}

func CaptureStdOut(t testing.TB, f func()) string {
r, out, err := os.Pipe()
if err != nil {
Expand Down
29 changes: 29 additions & 0 deletions internal/lsp/cmd/test/suggested_fix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmdtest

import (
"testing"

"golang.org/x/tools/internal/lsp/cmd"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/tool"
)

func (r *runner) SuggestedFix(t *testing.T, spn span.Span) {
uri := spn.URI()
filename := uri.Filename()
args := []string{"fix", "-a", filename}
app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options)
got := CaptureStdOut(t, func() {
_ = tool.Run(r.ctx, app, args)
})
want := string(r.data.Golden("suggestedfix", filename, func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
t.Errorf("suggested fixes failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
}
}