mirror of https://github.com/golang/go.git
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:
parent
0e1f6f5c09
commit
37a045f3b9
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue