internal/lsp: move undeclaredname suggested fix out of analysis

This CL is a follow-up from CL 241983. I didn't realize that the
undeclaredname analysis was also using the go/printer.Fprint trick,
which we decided was both incorrect and inefficient. This CL does
approximately the same things as CL 241983, with a few changes to make
the approach more general.

source.Analyzer now has a field to indicate if its suggested fix needs
to be computed separately, and that is used to determine which
code actions get commands. We also make helper functions to map
analyses to their commands.

I figured out a neater way to test suggested fixes in this CL, so I
reversed the move to source_test back to lsp_test (which was the right
place all along).

Change-Id: I505bf4790481d887edda8b82897e541ec73fb427
Reviewed-on: https://go-review.googlesource.com/c/tools/+/242366
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rebecca Stambler 2020-07-13 19:36:26 -04:00
parent 0e1f6f5c09
commit 37a045f3b9
14 changed files with 281 additions and 214 deletions

View File

@ -1,32 +0,0 @@
// Copyright 2020 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 undeclared
func x() int {
var z int
y :=
z = y // want "undeclared name: y"
m :=
if z == m { // want "undeclared name: m"
z = 1
}
n :=
if z == 1 {
z = 1
} else if z == n+1 { // want "undeclared name: n"
z = 1
}
a :=
switch z {
case 10:
z = 1
case a: // want "undeclared name: a"
z = 1
}
return z
}

View File

@ -10,7 +10,8 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"go/ast" "go/ast"
"go/printer" "go/token"
"go/types"
"strings" "strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
@ -65,51 +66,61 @@ func run(pass *analysis.Pass) (interface{}, error) {
if _, ok := path[1].(*ast.SelectorExpr); ok { if _, ok := path[1].(*ast.SelectorExpr); ok {
continue continue
} }
// TODO(golang.org/issue/34644): in a follow up handle call expressions // TODO(golang.org/issue/34644): Handle call expressions with suggested
// with suggested fix to create function // fixes to create a function.
if _, ok := path[1].(*ast.CallExpr); ok { if _, ok := path[1].(*ast.CallExpr); ok {
continue continue
} }
tok := pass.Fset.File(file.Pos())
// Get the place to insert the new statement. if tok == nil {
insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path)
if insertBeforeStmt == nil {
continue continue
} }
offset := pass.Fset.Position(err.Pos).Offset
var buf bytes.Buffer end := tok.Pos(offset + len(name))
if err := printer.Fprint(&buf, pass.Fset, file); err != nil {
continue
}
old := buf.Bytes()
insertBefore := pass.Fset.Position(insertBeforeStmt.Pos()).Offset
// Get the indent to add on the line after the new statement.
// Since this will have a parse error, we can not use format.Source().
contentBeforeStmt, indent := old[:insertBefore], "\n"
if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 {
indent = string(contentBeforeStmt[nl:])
}
// Create the new local variable statement.
newStmt := fmt.Sprintf("%s := %s", ident.Name, indent)
pass.Report(analysis.Diagnostic{ pass.Report(analysis.Diagnostic{
Pos: err.Pos, Pos: err.Pos,
End: analysisinternal.TypeErrorEndPos(pass.Fset, old, err.Pos), End: end,
Message: err.Msg, Message: err.Msg,
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Create variable \"%s\"", ident.Name),
TextEdits: []analysis.TextEdit{{
Pos: insertBeforeStmt.Pos(),
End: insertBeforeStmt.Pos(),
NewText: []byte(newStmt),
}},
}},
}) })
} }
return nil, nil return nil, nil
} }
func SuggestedFix(fset *token.FileSet, pos token.Pos, content []byte, file *ast.File, _ *types.Package, _ *types.Info) (*analysis.SuggestedFix, error) {
path, _ := astutil.PathEnclosingInterval(file, pos, pos)
if len(path) < 2 {
return nil, fmt.Errorf("")
}
ident, ok := path[0].(*ast.Ident)
if !ok {
return nil, fmt.Errorf("")
}
// Get the place to insert the new statement.
insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path)
if insertBeforeStmt == nil {
return nil, fmt.Errorf("")
}
insertBefore := fset.Position(insertBeforeStmt.Pos()).Offset
// Get the indent to add on the line after the new statement.
// Since this will have a parse error, we can not use format.Source().
contentBeforeStmt, indent := content[:insertBefore], "\n"
if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 {
indent = string(contentBeforeStmt[nl:])
}
// Create the new local variable statement.
newStmt := fmt.Sprintf("%s := %s", ident.Name, indent)
return &analysis.SuggestedFix{
Message: fmt.Sprintf("Create variable \"%s\"", ident.Name),
TextEdits: []analysis.TextEdit{{
Pos: insertBeforeStmt.Pos(),
End: insertBeforeStmt.Pos(),
NewText: []byte(newStmt),
}},
}, nil
}
func FixesError(msg string) bool { func FixesError(msg string) bool {
return strings.HasPrefix(msg, undeclaredNamePrefix) return strings.HasPrefix(msg, undeclaredNamePrefix)
} }

View File

@ -13,5 +13,5 @@ import (
func Test(t *testing.T) { func Test(t *testing.T) {
testdata := analysistest.TestData() testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, undeclaredname.Analyzer, "a") analysistest.Run(t, testdata, undeclaredname.Analyzer, "a")
} }

View File

@ -120,21 +120,20 @@ func run(pass *analysis.Pass) (interface{}, error) {
if nObj := pass.TypesInfo.ObjectOf(n); nObj != param.typObj { if nObj := pass.TypesInfo.ObjectOf(n); nObj != param.typObj {
return false return false
} }
if _, ok := unused[param]; ok { delete(unused, param)
delete(unused, param)
}
return false return false
}) })
// Create the reports for the unused parameters. // Create the reports for the unused parameters.
for u, _ := range unused { for u := range unused {
start, end := u.field.Pos(), u.field.End() start, end := u.field.Pos(), u.field.End()
if len(u.field.Names) > 1 { if len(u.field.Names) > 1 {
start, end = u.ident.Pos(), u.ident.End() start, end = u.ident.Pos(), u.ident.End()
} }
// TODO(golang/go#36602): add suggested fixes to automatically remove the unused parameter, // TODO(golang/go#36602): Add suggested fixes to automatically
// to start, just remove it from the function declaration, // remove the unused parameter. To start, just remove it from the
// later, remove from every use of this function // function declaration. Later, remove it from every use of this
// function.
pass.Report(analysis.Diagnostic{ pass.Report(analysis.Diagnostic{
Pos: start, Pos: start,
End: end, End: end,

View File

@ -74,8 +74,8 @@ type Application struct {
PrepareOptions func(*source.Options) PrepareOptions func(*source.Options)
} }
func (a *Application) verbose() bool { func (app *Application) verbose() bool {
return a.Verbose || a.VeryVerbose return app.Verbose || app.VeryVerbose
} }
// New returns a new Application ready to run. // New returns a new Application ready to run.
@ -188,7 +188,7 @@ func (app *Application) featureCommands() []tool.Application {
&references{app: app}, &references{app: app},
&rename{app: app}, &rename{app: app},
&signature{app: app}, &signature{app: app},
&suggestedfix{app: app}, &suggestedFix{app: app},
&symbols{app: app}, &symbols{app: app},
&workspaceSymbol{app: app}, &workspaceSymbol{app: app},
} }

View File

@ -18,8 +18,8 @@ import (
errors "golang.org/x/xerrors" errors "golang.org/x/xerrors"
) )
// suggestedfix implements the fix verb for gopls. // suggestedFix implements the fix verb for gopls.
type suggestedfix struct { type suggestedFix struct {
Diff bool `flag:"d" help:"display diffs instead of rewriting files"` Diff bool `flag:"d" help:"display diffs instead of rewriting files"`
Write bool `flag:"w" help:"write result to (source) file instead of stdout"` 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"` All bool `flag:"a" help:"apply all fixes, not just preferred fixes"`
@ -27,10 +27,10 @@ type suggestedfix struct {
app *Application app *Application
} }
func (s *suggestedfix) Name() string { return "fix" } func (s *suggestedFix) Name() string { return "fix" }
func (s *suggestedfix) Usage() string { return "<filename>" } func (s *suggestedFix) Usage() string { return "<filename>" }
func (s *suggestedfix) ShortHelp() string { return "apply suggested fixes" } func (s *suggestedFix) ShortHelp() string { return "apply suggested fixes" }
func (s *suggestedfix) DetailedHelp(f *flag.FlagSet) { func (s *suggestedFix) DetailedHelp(f *flag.FlagSet) {
fmt.Fprintf(f.Output(), ` fmt.Fprintf(f.Output(), `
Example: apply suggested fixes for this file: Example: apply suggested fixes for this file:
@ -45,7 +45,7 @@ gopls fix flags are:
// - if -w is specified, updates the file in place; // - if -w is specified, updates the file in place;
// - if -d is specified, prints out unified diffs of the changes; or // - if -d is specified, prints out unified diffs of the changes; or
// - otherwise, prints the new versions to stdout. // - otherwise, prints the new versions to stdout.
func (s *suggestedfix) Run(ctx context.Context, args ...string) error { func (s *suggestedFix) Run(ctx context.Context, args ...string) error {
if len(args) < 1 { if len(args) < 1 {
return tool.CommandLineErrorf("fix expects at least 1 argument") return tool.CommandLineErrorf("fix expects at least 1 argument")
} }
@ -96,6 +96,9 @@ func (s *suggestedfix) Run(ctx context.Context, args ...string) error {
} }
var edits []protocol.TextEdit var edits []protocol.TextEdit
for _, a := range actions { for _, a := range actions {
if a.Command != nil {
return fmt.Errorf("ExecuteCommand is not yet supported on the command line")
}
if !a.IsPreferred && !s.All { if !a.IsPreferred && !s.All {
continue continue
} }

View File

@ -22,11 +22,14 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string)
} }
} }
args = append(args, actionKinds...) args = append(args, actionKinds...)
got, _ := r.NormalizeGoplsCmd(t, args...) got, stderr := r.NormalizeGoplsCmd(t, args...)
if stderr == "ExecuteCommand is not yet supported on the command line" {
t.Skipf(stderr)
}
want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) { want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) {
return []byte(got), nil return []byte(got), nil
})) }))
if want != got { if want != got {
t.Errorf("suggested fixes failed for %s: %s", filename, tests.Diff(want, got)) t.Errorf("suggested fixes failed for %s:\n%s", filename, tests.Diff(want, got))
} }
} }

View File

@ -13,7 +13,6 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/analysis/fillstruct"
"golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
@ -242,15 +241,25 @@ func analysisFixes(ctx context.Context, snapshot source.Snapshot, ph source.Pack
if len(diagnostics) == 0 { if len(diagnostics) == 0 {
return nil, nil, nil return nil, nil, nil
} }
var (
var codeActions []protocol.CodeAction codeActions []protocol.CodeAction
var sourceFixAllEdits []protocol.TextDocumentEdit sourceFixAllEdits []protocol.TextDocumentEdit
)
for _, diag := range diagnostics { for _, diag := range diagnostics {
srcErr, analyzer, ok := findSourceError(ctx, snapshot, ph.ID(), diag) srcErr, analyzer, ok := findSourceError(ctx, snapshot, ph.ID(), diag)
if !ok { if !ok {
continue continue
} }
// If the suggested fix for the diagnostic is expected to be separate,
// see if there are any supported commands available.
if analyzer.SuggestedFix != nil {
action, err := diagnosticToCommandCodeAction(ctx, snapshot, srcErr, &diag, protocol.QuickFix)
if err != nil {
return nil, nil, err
}
codeActions = append(codeActions, *action)
continue
}
for _, fix := range srcErr.SuggestedFixes { for _, fix := range srcErr.SuggestedFixes {
action := protocol.CodeAction{ action := protocol.CodeAction{
Title: fix.Title, Title: fix.Title,
@ -276,24 +285,8 @@ func analysisFixes(ctx context.Context, snapshot source.Snapshot, ph source.Pack
} }
func findSourceError(ctx context.Context, snapshot source.Snapshot, pkgID string, diag protocol.Diagnostic) (*source.Error, source.Analyzer, bool) { func findSourceError(ctx context.Context, snapshot source.Snapshot, pkgID string, diag protocol.Diagnostic) (*source.Error, source.Analyzer, bool) {
var analyzer *source.Analyzer analyzer := diagnosticToAnalyzer(snapshot, diag.Source, diag.Message)
if analyzer == nil {
// If the source is "compiler", we expect a type error analyzer.
if diag.Source == "compiler" {
for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
if a.FixesError(diag.Message) {
analyzer = &a
break
}
}
} else {
// This code assumes that the analyzer name is the Source of the diagnostic.
// If this ever changes, this will need to be addressed.
if a, ok := snapshot.View().Options().DefaultAnalyzers[diag.Source]; ok {
analyzer = &a
}
}
if analyzer == nil || !analyzer.Enabled(snapshot) {
return nil, source.Analyzer{}, false return nil, source.Analyzer{}, false
} }
analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer) analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer)
@ -316,12 +309,48 @@ func findSourceError(ctx context.Context, snapshot source.Snapshot, pkgID string
return nil, source.Analyzer{}, false return nil, source.Analyzer{}, false
} }
// diagnosticToAnalyzer return the analyzer associated with a given diagnostic.
// It assumes that the diagnostic's source will be the name of the analyzer.
// If this changes, this approach will need to be reworked.
func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *source.Analyzer) {
// Make sure that the analyzer we found is enabled.
defer func() {
if analyzer != nil && !analyzer.Enabled(snapshot) {
analyzer = nil
}
}()
if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok {
return &a
}
if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok {
return &a
}
// Hack: We publish diagnostics with the source "compiler" for type errors,
// but these analyzers have different names. Try both possibilities.
if a, ok := snapshot.View().Options().TypeErrorAnalyzers[src]; ok {
return &a
}
if src != "compiler" {
return nil
}
for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
if a.FixesError(msg) {
return &a
}
}
return nil
}
func convenienceFixes(ctx context.Context, snapshot source.Snapshot, ph source.PackageHandle, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { func convenienceFixes(ctx context.Context, snapshot source.Snapshot, ph source.PackageHandle, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
var analyzers []*analysis.Analyzer var analyzers []*analysis.Analyzer
for _, a := range snapshot.View().Options().ConvenienceAnalyzers { for _, a := range snapshot.View().Options().ConvenienceAnalyzers {
if !a.Enabled(snapshot) { if !a.Enabled(snapshot) {
continue continue
} }
if a.SuggestedFix == nil {
event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
continue
}
analyzers = append(analyzers, a.Analyzer) analyzers = append(analyzers, a.Analyzer)
} }
diagnostics, err := snapshot.Analyze(ctx, ph.ID(), analyzers...) diagnostics, err := snapshot.Analyze(ctx, ph.ID(), analyzers...)
@ -338,29 +367,45 @@ func convenienceFixes(ctx context.Context, snapshot source.Snapshot, ph source.P
if d.Range.Start.Line != rng.Start.Line { if d.Range.Start.Line != rng.Start.Line {
continue continue
} }
// The fix depends on the category of the analyzer. action, err := diagnosticToCommandCodeAction(ctx, snapshot, d, nil, protocol.RefactorRewrite)
switch d.Category { if err != nil {
case fillstruct.Analyzer.Name: return nil, err
jsonArgs, err := source.EncodeArgs(d.URI, d.Range)
if err != nil {
return nil, err
}
action := protocol.CodeAction{
Title: d.Message,
Kind: protocol.RefactorRewrite,
Command: &protocol.Command{
Command: source.CommandFillStruct,
Title: d.Message,
Arguments: jsonArgs,
},
}
codeActions = append(codeActions, action)
} }
codeActions = append(codeActions, *action)
} }
return codeActions, nil return codeActions, nil
} }
func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot, e *source.Error, d *protocol.Diagnostic, kind protocol.CodeActionKind) (*protocol.CodeAction, error) {
// The fix depends on the category of the analyzer. The diagnostic may be
// nil, so use the error's category.
analyzer := diagnosticToAnalyzer(snapshot, e.Category, e.Message)
if analyzer == nil {
return nil, fmt.Errorf("no convenience analyzer for category %s", e.Category)
}
if analyzer.Command == "" {
return nil, fmt.Errorf("no command for convenience analyzer %s", analyzer.Analyzer.Name)
}
jsonArgs, err := source.EncodeArgs(e.URI, e.Range)
if err != nil {
return nil, err
}
var diagnostics []protocol.Diagnostic
if d != nil {
diagnostics = append(diagnostics, *d)
}
return &protocol.CodeAction{
Title: e.Message,
Kind: kind,
Diagnostics: diagnostics,
Command: &protocol.Command{
Command: analyzer.Command,
Title: e.Message,
Arguments: jsonArgs,
},
}, nil
}
func extractionFixes(ctx context.Context, snapshot source.Snapshot, ph source.PackageHandle, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { func extractionFixes(ctx context.Context, snapshot source.Snapshot, ph source.PackageHandle, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
fh, err := snapshot.GetFile(ctx, uri) fh, err := snapshot.GetFile(ctx, uri)
if err != nil { if err != nil {

View File

@ -100,7 +100,7 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
} }
err := s.directGoModCommand(ctx, uri, "get", deps...) err := s.directGoModCommand(ctx, uri, "get", deps...)
return nil, err return nil, err
case source.CommandFillStruct: case source.CommandFillStruct, source.CommandUndeclaredName:
var uri protocol.DocumentURI var uri protocol.DocumentURI
var rng protocol.Range var rng protocol.Range
if err := source.DecodeArgs(params.Arguments, &uri, &rng); err != nil { if err := source.DecodeArgs(params.Arguments, &uri, &rng); err != nil {
@ -110,7 +110,7 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
if !ok { if !ok {
return nil, err return nil, err
} }
edits, err := source.FillStruct(ctx, snapshot, fh, rng) edits, err := commandToEdits(ctx, snapshot, fh, rng, params.Command)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,7 +125,7 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
if !r.Applied { if !r.Applied {
return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Error, Type: protocol.Error,
Message: fmt.Sprintf("fillstruct failed: %v", r.FailureReason), Message: fmt.Sprintf("%s failed: %v", params.Command, r.FailureReason),
}) })
} }
default: default:
@ -134,6 +134,23 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
return nil, nil return nil, nil
} }
func commandToEdits(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, rng protocol.Range, cmd string) ([]protocol.TextDocumentEdit, error) {
var analyzer *source.Analyzer
for _, a := range source.EnabledAnalyzers(snapshot) {
if cmd == a.Command {
analyzer = &a
break
}
}
if analyzer == nil {
return nil, fmt.Errorf("no known analyzer for %s", cmd)
}
if analyzer.SuggestedFix == nil {
return nil, fmt.Errorf("no fix function for %s", cmd)
}
return source.CommandSuggestedFixes(ctx, snapshot, fh, rng, analyzer.SuggestedFix)
}
func (s *Server) directGoModCommand(ctx context.Context, uri protocol.DocumentURI, verb string, args ...string) error { func (s *Server) directGoModCommand(ctx context.Context, uri protocol.DocumentURI, verb string, args ...string) error {
view, err := s.session.ViewOf(uri.SpanURI()) view, err := s.session.ViewOf(uri.SpanURI())
if err != nil { if err != nil {

View File

@ -352,7 +352,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
} }
got := string(m.Content) got := string(m.Content)
if len(actions) > 0 { if len(actions) > 0 {
res, err := applyWorkspaceEdits(r, actions[0].Edit) res, err := applyTextDocumentEdits(r, actions[0].Edit.DocumentChanges)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -373,6 +373,11 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
snapshot := view.Snapshot()
fh, err := snapshot.GetFile(r.ctx, uri)
if err != nil {
t.Fatal(err)
}
m, err := r.data.Mapper(uri) m, err := r.data.Mapper(uri)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -417,17 +422,37 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string)
if err != nil { if err != nil {
t.Fatalf("CodeAction %s failed: %v", spn, err) t.Fatalf("CodeAction %s failed: %v", spn, err)
} }
// Hack: We assume that we only get one code action per range. if len(actions) != 1 {
// TODO(rstambler): Support multiple code actions per test. // Hack: We assume that we only get one code action per range.
if len(actions) == 0 || len(actions) > 1 { // TODO(rstambler): Support multiple code actions per test.
t.Fatalf("unexpected number of code actions, want 1, got %v (%v)", len(actions), actions) t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions))
} }
if actions[0].Command != nil { action := actions[0]
t.Skip("no tests for code action commands") var match bool
for _, k := range codeActionKinds {
if action.Kind == k {
match = true
break
}
} }
res, err := applyWorkspaceEdits(r, actions[0].Edit) if !match {
if err != nil { t.Fatalf("unexpected kind for code action %s, expected one of %v, got %v", action.Title, codeActionKinds, action.Kind)
t.Fatal(err) }
var res map[span.URI]string
if cmd := action.Command; cmd != nil {
edits, err := commandToEdits(r.ctx, snapshot, fh, rng, action.Command.Command)
if err != nil {
t.Fatal(err)
}
res, err = applyTextDocumentEdits(r, edits)
if err != nil {
t.Fatal(err)
}
} else {
res, err = applyTextDocumentEdits(r, action.Edit.DocumentChanges)
if err != nil {
t.Fatal(err)
}
} }
for u, got := range res { for u, got := range res {
want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) {
@ -471,7 +496,7 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span
if len(actions) == 0 || len(actions) > 1 { if len(actions) == 0 || len(actions) > 1 {
t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions)) t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions))
} }
res, err := applyWorkspaceEdits(r, actions[0].Edit) res, err := applyTextDocumentEdits(r, actions[0].Edit.DocumentChanges)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -731,7 +756,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) {
} }
return return
} }
res, err := applyWorkspaceEdits(r, *wedit) res, err := applyTextDocumentEdits(r, wedit.DocumentChanges)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -802,9 +827,9 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare
} }
} }
func applyWorkspaceEdits(r *runner, wedit protocol.WorkspaceEdit) (map[span.URI]string, error) { func applyTextDocumentEdits(r *runner, edits []protocol.TextDocumentEdit) (map[span.URI]string, error) {
res := map[span.URI]string{} res := map[span.URI]string{}
for _, docEdits := range wedit.DocumentChanges { for _, docEdits := range edits {
uri := docEdits.TextDocument.URI.SpanURI() uri := docEdits.TextDocument.URI.SpanURI()
m, err := r.data.Mapper(uri) m, err := r.data.Mapper(uri)
if err != nil { if err != nil {

View File

@ -76,6 +76,10 @@ const (
// CommandFillStruct is a gopls command to fill a struct with default // CommandFillStruct is a gopls command to fill a struct with default
// values. // values.
CommandFillStruct = "fill_struct" CommandFillStruct = "fill_struct"
// CommandUndeclaredName is a gopls command to add a variable declaration
// for an undeclared name.
CommandUndeclaredName = "undeclared_name"
) )
// DefaultOptions is the options that are used for Gopls execution independent // DefaultOptions is the options that are used for Gopls execution independent
@ -112,6 +116,7 @@ func DefaultOptions() Options {
CommandRegenerateCgo, CommandRegenerateCgo,
CommandTest, CommandTest,
CommandTidy, CommandTidy,
CommandUndeclaredName,
CommandUpgradeDependency, CommandUpgradeDependency,
CommandVendor, CommandVendor,
}, },
@ -683,28 +688,51 @@ func (r *OptionResult) setString(s *string) {
} }
} }
// EnabledAnalyzers returns all of the analyzers enabled for the given
// snapshot.
func EnabledAnalyzers(snapshot Snapshot) (analyzers []Analyzer) {
for _, a := range snapshot.View().Options().DefaultAnalyzers {
if a.Enabled(snapshot) {
analyzers = append(analyzers, a)
}
}
for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
if a.Enabled(snapshot) {
analyzers = append(analyzers, a)
}
}
for _, a := range snapshot.View().Options().ConvenienceAnalyzers {
if a.Enabled(snapshot) {
analyzers = append(analyzers, a)
}
}
return analyzers
}
func typeErrorAnalyzers() map[string]Analyzer { func typeErrorAnalyzers() map[string]Analyzer {
return map[string]Analyzer{ return map[string]Analyzer{
fillreturns.Analyzer.Name: { fillreturns.Analyzer.Name: {
Analyzer: fillreturns.Analyzer, Analyzer: fillreturns.Analyzer,
enabled: true,
FixesError: fillreturns.FixesError, FixesError: fillreturns.FixesError,
HighConfidence: true, HighConfidence: true,
enabled: true,
}, },
nonewvars.Analyzer.Name: { nonewvars.Analyzer.Name: {
Analyzer: nonewvars.Analyzer, Analyzer: nonewvars.Analyzer,
enabled: true,
FixesError: nonewvars.FixesError, FixesError: nonewvars.FixesError,
enabled: true,
}, },
noresultvalues.Analyzer.Name: { noresultvalues.Analyzer.Name: {
Analyzer: noresultvalues.Analyzer, Analyzer: noresultvalues.Analyzer,
enabled: true,
FixesError: noresultvalues.FixesError, FixesError: noresultvalues.FixesError,
enabled: true,
}, },
undeclaredname.Analyzer.Name: { undeclaredname.Analyzer.Name: {
Analyzer: undeclaredname.Analyzer, Analyzer: undeclaredname.Analyzer,
enabled: true, FixesError: undeclaredname.FixesError,
FixesError: undeclaredname.FixesError, SuggestedFix: undeclaredname.SuggestedFix,
Command: CommandUndeclaredName,
enabled: true,
}, },
} }
} }
@ -712,8 +740,10 @@ func typeErrorAnalyzers() map[string]Analyzer {
func convenienceAnalyzers() map[string]Analyzer { func convenienceAnalyzers() map[string]Analyzer {
return map[string]Analyzer{ return map[string]Analyzer{
fillstruct.Analyzer.Name: { fillstruct.Analyzer.Name: {
Analyzer: fillstruct.Analyzer, Analyzer: fillstruct.Analyzer,
enabled: true, SuggestedFix: fillstruct.SuggestedFix,
Command: CommandFillStruct,
enabled: true,
}, },
} }
} }

View File

@ -474,63 +474,6 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
} }
} }
func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {
var refactorRewrite bool
for _, actionKind := range actionKinds {
if actionKind == "refactor.rewrite" {
refactorRewrite = true
break
}
}
// Only test refactor rewrite code actions.
if !refactorRewrite {
return
}
snapshot := r.view.Snapshot()
fh, err := snapshot.GetFile(r.ctx, spn.URI())
if err != nil {
t.Fatal(err)
}
edits, err := source.FillStruct(r.ctx, snapshot, fh, protocol.Range{
Start: protocol.Position{
Line: float64(spn.Start().Line() - 1),
Character: float64(spn.Start().Column() - 1),
},
End: protocol.Position{
Line: float64(spn.End().Line() - 1),
Character: float64(spn.End().Column() - 1),
},
})
if err != nil {
t.Fatal(err)
}
m, err := r.data.Mapper(fh.URI())
if err != nil {
t.Fatal(err)
}
if len(edits) == 0 || len(edits) > 1 {
t.Fatalf("expected 1 edit, got %v", len(edits))
}
diffEdits, err := source.FromProtocolEdits(m, edits[0].Edits)
if err != nil {
t.Error(err)
}
data, err := fh.Read()
if err != nil {
t.Fatal(err)
}
got := diff.ApplyEdits(string(data), diffEdits)
want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), fh.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
d := myers.ComputeEdits(spn.URI(), want, got)
t.Errorf("import failed for %s: %s", spn.URI().Filename(), diff.ToUnified("want", "got", want, d))
}
}
func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {}
func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
_, srcRng, err := spanToRange(r.data, d.Src) _, srcRng, err := spanToRange(r.data, d.Src)
if err != nil { if err != nil {
@ -918,9 +861,10 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa
} }
} }
func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { // These are pure LSP features, no source level functionality to be tested.
// This is a pure LSP feature, no source level functionality to be tested. func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {}
} func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {}
func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {}
func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) { func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
fh, err := r.view.Snapshot().GetFile(r.ctx, uri) fh, err := r.view.Snapshot().GetFile(r.ctx, uri)

View File

@ -7,13 +7,26 @@ package source
import ( import (
"context" "context"
"fmt" "fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/internal/lsp/analysis/fillstruct" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
func FillStruct(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) { // SuggestedFixFunc is a function used to get the suggested fixes for a given
// go/analysis.Analyzer. Some of the analyzers in internal/lsp/analysis are not
// efficient enough to include suggested fixes with their diagnostics, so we
// have to compute them separately. Such analyzers should provide a function
// with a signature of SuggestedFixFunc.
type SuggestedFixFunc func(*token.FileSet, token.Pos, []byte, *ast.File, *types.Package, *types.Info) (*analysis.SuggestedFix, error)
// CommandSuggestedFixes returns the text edits for a given file and
// SuggestedFixFunc. It can be used to execute any command that provides its
// edits through a SuggestedFixFunc.
func CommandSuggestedFixes(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range, fn SuggestedFixFunc) ([]protocol.TextDocumentEdit, error) {
pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle) pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting file for Identifier: %w", err) return nil, fmt.Errorf("getting file for Identifier: %w", err)
@ -35,7 +48,7 @@ func FillStruct(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng prot
return nil, err return nil, err
} }
fset := snapshot.View().Session().Cache().FileSet() fset := snapshot.View().Session().Cache().FileSet()
fix, err := fillstruct.SuggestedFix(fset, rng.Start, content, file, pkg.GetTypes(), pkg.GetTypesInfo()) fix, err := fn(fset, rng.Start, content, file, pkg.GetTypes(), pkg.GetTypesInfo())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -460,6 +460,15 @@ type Analyzer struct {
Analyzer *analysis.Analyzer Analyzer *analysis.Analyzer
enabled bool enabled bool
// SuggestedFix is non-nil if we expect this analyzer to provide its fix
// separately from its diagnostics. That is, we should apply the analyzer's
// suggested fixes through a Command, not a TextEdit.
SuggestedFix SuggestedFixFunc
// Command is the name of the command used to invoke the suggested fixes
// for the analyzer.
Command string
// If this is true, then we can apply the suggested fixes // If this is true, then we can apply the suggested fixes
// as part of a source.FixAll codeaction. // as part of a source.FixAll codeaction.
HighConfidence bool HighConfidence bool