golang.org/x/tools/gopls@v0.15.3/internal/golang/codeaction.go (about)

     1  // Copyright 2024 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  
     5  package golang
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"go/ast"
    12  	"strings"
    13  
    14  	"golang.org/x/tools/go/ast/inspector"
    15  	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
    16  	"golang.org/x/tools/gopls/internal/cache"
    17  	"golang.org/x/tools/gopls/internal/cache/parsego"
    18  	"golang.org/x/tools/gopls/internal/file"
    19  	"golang.org/x/tools/gopls/internal/protocol"
    20  	"golang.org/x/tools/gopls/internal/protocol/command"
    21  	"golang.org/x/tools/gopls/internal/settings"
    22  	"golang.org/x/tools/gopls/internal/util/bug"
    23  	"golang.org/x/tools/gopls/internal/util/slices"
    24  	"golang.org/x/tools/internal/event"
    25  	"golang.org/x/tools/internal/event/tag"
    26  	"golang.org/x/tools/internal/imports"
    27  )
    28  
    29  // CodeActions returns all code actions (edits and other commands)
    30  // available for the selected range.
    31  func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) (actions []protocol.CodeAction, _ error) {
    32  	// Only compute quick fixes if there are any diagnostics to fix.
    33  	wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0
    34  
    35  	// Code actions requiring syntax information alone.
    36  	if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] {
    37  		pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull)
    38  		if err != nil {
    39  			return nil, err
    40  		}
    41  
    42  		// Process any missing imports and pair them with the diagnostics they fix.
    43  		if wantQuickFixes || want[protocol.SourceOrganizeImports] {
    44  			importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf)
    45  			if err != nil {
    46  				event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Path()))
    47  				importEdits = nil
    48  				importEditsPerFix = nil
    49  			}
    50  
    51  			// Separate this into a set of codeActions per diagnostic, where
    52  			// each action is the addition, removal, or renaming of one import.
    53  			if wantQuickFixes {
    54  				for _, importFix := range importEditsPerFix {
    55  					fixed := fixedByImportFix(importFix.fix, diagnostics)
    56  					if len(fixed) == 0 {
    57  						continue
    58  					}
    59  					actions = append(actions, protocol.CodeAction{
    60  						Title: importFixTitle(importFix.fix),
    61  						Kind:  protocol.QuickFix,
    62  						Edit: &protocol.WorkspaceEdit{
    63  							DocumentChanges: documentChanges(fh, importFix.edits),
    64  						},
    65  						Diagnostics: fixed,
    66  					})
    67  				}
    68  			}
    69  
    70  			// Send all of the import edits as one code action if the file is
    71  			// being organized.
    72  			if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
    73  				actions = append(actions, protocol.CodeAction{
    74  					Title: "Organize Imports",
    75  					Kind:  protocol.SourceOrganizeImports,
    76  					Edit: &protocol.WorkspaceEdit{
    77  						DocumentChanges: documentChanges(fh, importEdits),
    78  					},
    79  				})
    80  			}
    81  		}
    82  
    83  		if want[protocol.RefactorExtract] {
    84  			extractions, err := getExtractCodeActions(pgf, rng, snapshot.Options())
    85  			if err != nil {
    86  				return nil, err
    87  			}
    88  			actions = append(actions, extractions...)
    89  		}
    90  	}
    91  
    92  	// Code actions requiring type information.
    93  	if want[protocol.RefactorRewrite] ||
    94  		want[protocol.RefactorInline] ||
    95  		want[protocol.GoTest] {
    96  		pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  		if want[protocol.RefactorRewrite] {
   101  			rewrites, err := getRewriteCodeActions(pkg, pgf, fh, rng, snapshot.Options())
   102  			if err != nil {
   103  				return nil, err
   104  			}
   105  			actions = append(actions, rewrites...)
   106  		}
   107  
   108  		if want[protocol.RefactorInline] {
   109  			rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options())
   110  			if err != nil {
   111  				return nil, err
   112  			}
   113  			actions = append(actions, rewrites...)
   114  		}
   115  
   116  		if want[protocol.GoTest] {
   117  			fixes, err := getGoTestCodeActions(pkg, pgf, rng)
   118  			if err != nil {
   119  				return nil, err
   120  			}
   121  			actions = append(actions, fixes...)
   122  		}
   123  	}
   124  	return actions, nil
   125  }
   126  
   127  func supportsResolveEdits(options *settings.Options) bool {
   128  	return options.CodeActionResolveOptions != nil && slices.Contains(options.CodeActionResolveOptions, "edit")
   129  }
   130  
   131  func importFixTitle(fix *imports.ImportFix) string {
   132  	var str string
   133  	switch fix.FixType {
   134  	case imports.AddImport:
   135  		str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
   136  	case imports.DeleteImport:
   137  		str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
   138  	case imports.SetImportName:
   139  		str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
   140  	}
   141  	return str
   142  }
   143  
   144  // fixedByImportFix filters the provided slice of diagnostics to those that
   145  // would be fixed by the provided imports fix.
   146  func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic {
   147  	var results []protocol.Diagnostic
   148  	for _, diagnostic := range diagnostics {
   149  		switch {
   150  		// "undeclared name: X" may be an unresolved import.
   151  		case strings.HasPrefix(diagnostic.Message, "undeclared name: "):
   152  			ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ")
   153  			if ident == fix.IdentName {
   154  				results = append(results, diagnostic)
   155  			}
   156  		// "undefined: X" may be an unresolved import at Go 1.20+.
   157  		case strings.HasPrefix(diagnostic.Message, "undefined: "):
   158  			ident := strings.TrimPrefix(diagnostic.Message, "undefined: ")
   159  			if ident == fix.IdentName {
   160  				results = append(results, diagnostic)
   161  			}
   162  		// "could not import: X" may be an invalid import.
   163  		case strings.HasPrefix(diagnostic.Message, "could not import: "):
   164  			ident := strings.TrimPrefix(diagnostic.Message, "could not import: ")
   165  			if ident == fix.IdentName {
   166  				results = append(results, diagnostic)
   167  			}
   168  		// "X imported but not used" is an unused import.
   169  		// "X imported but not used as Y" is an unused import.
   170  		case strings.Contains(diagnostic.Message, " imported but not used"):
   171  			idx := strings.Index(diagnostic.Message, " imported but not used")
   172  			importPath := diagnostic.Message[:idx]
   173  			if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) {
   174  				results = append(results, diagnostic)
   175  			}
   176  		}
   177  	}
   178  	return results
   179  }
   180  
   181  // getExtractCodeActions returns any refactor.extract code actions for the selection.
   182  func getExtractCodeActions(pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) {
   183  	if rng.Start == rng.End {
   184  		return nil, nil
   185  	}
   186  
   187  	start, end, err := pgf.RangePos(rng)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	puri := pgf.URI
   192  	var commands []protocol.Command
   193  	if _, ok, methodOk, _ := CanExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok {
   194  		cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{
   195  			Fix:          fixExtractFunction,
   196  			URI:          puri,
   197  			Range:        rng,
   198  			ResolveEdits: supportsResolveEdits(options),
   199  		})
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  		commands = append(commands, cmd)
   204  		if methodOk {
   205  			cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{
   206  				Fix:          fixExtractMethod,
   207  				URI:          puri,
   208  				Range:        rng,
   209  				ResolveEdits: supportsResolveEdits(options),
   210  			})
   211  			if err != nil {
   212  				return nil, err
   213  			}
   214  			commands = append(commands, cmd)
   215  		}
   216  	}
   217  	if _, _, ok, _ := CanExtractVariable(start, end, pgf.File); ok {
   218  		cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{
   219  			Fix:          fixExtractVariable,
   220  			URI:          puri,
   221  			Range:        rng,
   222  			ResolveEdits: supportsResolveEdits(options),
   223  		})
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		commands = append(commands, cmd)
   228  	}
   229  	var actions []protocol.CodeAction
   230  	for i := range commands {
   231  		actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options))
   232  	}
   233  	return actions, nil
   234  }
   235  
   236  func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Command, diagnostics []protocol.Diagnostic, options *settings.Options) protocol.CodeAction {
   237  	action := protocol.CodeAction{
   238  		Title:       title,
   239  		Kind:        kind,
   240  		Diagnostics: diagnostics,
   241  	}
   242  	if !supportsResolveEdits(options) {
   243  		action.Command = cmd
   244  	} else {
   245  		data, err := json.Marshal(cmd)
   246  		if err != nil {
   247  			panic("unable to marshal")
   248  		}
   249  		msg := json.RawMessage(data)
   250  		action.Data = &msg
   251  	}
   252  	return action
   253  }
   254  
   255  // getRewriteCodeActions returns refactor.rewrite code actions available at the specified range.
   256  func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) {
   257  	// golang/go#61693: code actions were refactored to run outside of the
   258  	// analysis framework, but as a result they lost their panic recovery.
   259  	//
   260  	// These code actions should never fail, but put back the panic recovery as a
   261  	// defensive measure.
   262  	defer func() {
   263  		if r := recover(); r != nil {
   264  			rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r)
   265  		}
   266  	}()
   267  
   268  	var actions []protocol.CodeAction
   269  
   270  	if canRemoveParameter(pkg, pgf, rng) {
   271  		cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{
   272  			RemoveParameter: protocol.Location{
   273  				URI:   pgf.URI,
   274  				Range: rng,
   275  			},
   276  			ResolveEdits: supportsResolveEdits(options),
   277  		})
   278  		if err != nil {
   279  			return nil, err
   280  		}
   281  		actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options))
   282  	}
   283  
   284  	if action, ok := ConvertStringLiteral(pgf, fh, rng); ok {
   285  		actions = append(actions, action)
   286  	}
   287  
   288  	start, end, err := pgf.RangePos(rng)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	var commands []protocol.Command
   294  	if _, ok, _ := CanInvertIfCondition(pgf.File, start, end); ok {
   295  		cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{
   296  			Fix:          fixInvertIfCondition,
   297  			URI:          pgf.URI,
   298  			Range:        rng,
   299  			ResolveEdits: supportsResolveEdits(options),
   300  		})
   301  		if err != nil {
   302  			return nil, err
   303  		}
   304  		commands = append(commands, cmd)
   305  	}
   306  
   307  	// N.B.: an inspector only pays for itself after ~5 passes, which means we're
   308  	// currently not getting a good deal on this inspection.
   309  	//
   310  	// TODO: Consider removing the inspection after convenienceAnalyzers are removed.
   311  	inspect := inspector.New([]*ast.File{pgf.File})
   312  	for _, diag := range fillstruct.Diagnose(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) {
   313  		rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End)
   314  		if err != nil {
   315  			return nil, err
   316  		}
   317  		for _, fix := range diag.SuggestedFixes {
   318  			cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{
   319  				Fix:          diag.Category,
   320  				URI:          pgf.URI,
   321  				Range:        rng,
   322  				ResolveEdits: supportsResolveEdits(options),
   323  			})
   324  			if err != nil {
   325  				return nil, err
   326  			}
   327  			commands = append(commands, cmd)
   328  		}
   329  	}
   330  
   331  	for i := range commands {
   332  		actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options))
   333  	}
   334  
   335  	return actions, nil
   336  }
   337  
   338  // canRemoveParameter reports whether we can remove the function parameter
   339  // indicated by the given [start, end) range.
   340  //
   341  // This is true if:
   342  //   - [start, end) is contained within an unused field or parameter name
   343  //   - ... of a non-method function declaration.
   344  //
   345  // (Note that the unusedparam analyzer also computes this property, but
   346  // much more precisely, allowing it to report its findings as diagnostics.)
   347  func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) bool {
   348  	info, err := FindParam(pgf, rng)
   349  	if err != nil {
   350  		return false // e.g. invalid range
   351  	}
   352  	if info.Field == nil {
   353  		return false // range does not span a parameter
   354  	}
   355  	if info.Decl.Body == nil {
   356  		return false // external function
   357  	}
   358  	if len(info.Field.Names) == 0 {
   359  		return true // no names => field is unused
   360  	}
   361  	if info.Name == nil {
   362  		return false // no name is indicated
   363  	}
   364  	if info.Name.Name == "_" {
   365  		return true // trivially unused
   366  	}
   367  
   368  	obj := pkg.GetTypesInfo().Defs[info.Name]
   369  	if obj == nil {
   370  		return false // something went wrong
   371  	}
   372  
   373  	used := false
   374  	ast.Inspect(info.Decl.Body, func(node ast.Node) bool {
   375  		if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj {
   376  			used = true
   377  		}
   378  		return !used // keep going until we find a use
   379  	})
   380  	return !used
   381  }
   382  
   383  // getInlineCodeActions returns refactor.inline actions available at the specified range.
   384  func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) {
   385  	start, end, err := pgf.RangePos(rng)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	// If range is within call expression, offer inline action.
   391  	var commands []protocol.Command
   392  	if _, fn, err := EnclosingStaticCall(pkg, pgf, start, end); err == nil {
   393  		cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{
   394  			Fix:          fixInlineCall,
   395  			URI:          pgf.URI,
   396  			Range:        rng,
   397  			ResolveEdits: supportsResolveEdits(options),
   398  		})
   399  		if err != nil {
   400  			return nil, err
   401  		}
   402  		commands = append(commands, cmd)
   403  	}
   404  
   405  	// Convert commands to actions.
   406  	var actions []protocol.CodeAction
   407  	for i := range commands {
   408  		actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorInline, &commands[i], nil, options))
   409  	}
   410  	return actions, nil
   411  }
   412  
   413  // getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection.
   414  func getGoTestCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) {
   415  	fns, err := TestsAndBenchmarks(pkg, pgf)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	var tests, benchmarks []string
   421  	for _, fn := range fns.Tests {
   422  		if !protocol.Intersect(fn.Rng, rng) {
   423  			continue
   424  		}
   425  		tests = append(tests, fn.Name)
   426  	}
   427  	for _, fn := range fns.Benchmarks {
   428  		if !protocol.Intersect(fn.Rng, rng) {
   429  			continue
   430  		}
   431  		benchmarks = append(benchmarks, fn.Name)
   432  	}
   433  
   434  	if len(tests) == 0 && len(benchmarks) == 0 {
   435  		return nil, nil
   436  	}
   437  
   438  	cmd, err := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks)
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  	return []protocol.CodeAction{{
   443  		Title:   cmd.Title,
   444  		Kind:    protocol.GoTest,
   445  		Command: &cmd,
   446  	}}, nil
   447  }
   448  
   449  func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges {
   450  	return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits)
   451  }