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

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cmd
     6  
     7  import (
     8  	"context"
     9  	"flag"
    10  	"fmt"
    11  
    12  	"golang.org/x/tools/gopls/internal/protocol"
    13  	"golang.org/x/tools/gopls/internal/util/slices"
    14  	"golang.org/x/tools/internal/tool"
    15  )
    16  
    17  // TODO(adonovan): this command has a very poor user interface. It
    18  // should have a way to query the available fixes for a file (without
    19  // a span), enumerate the valid fix kinds, enable all fixes, and not
    20  // require the pointless -all flag. See issue #60290.
    21  
    22  // suggestedFix implements the fix verb for gopls.
    23  type suggestedFix struct {
    24  	EditFlags
    25  	All bool `flag:"a,all" help:"apply all fixes, not just preferred fixes"`
    26  
    27  	app *Application
    28  }
    29  
    30  func (s *suggestedFix) Name() string      { return "fix" }
    31  func (s *suggestedFix) Parent() string    { return s.app.Name() }
    32  func (s *suggestedFix) Usage() string     { return "[fix-flags] <filename>" }
    33  func (s *suggestedFix) ShortHelp() string { return "apply suggested fixes" }
    34  func (s *suggestedFix) DetailedHelp(f *flag.FlagSet) {
    35  	fmt.Fprintf(f.Output(), `
    36  Example: apply fixes to this file, rewriting it:
    37  
    38  	$ gopls fix -a -w internal/cmd/check.go
    39  
    40  The -a (-all) flag causes all fixes, not just preferred ones, to be
    41  applied, but since no fixes are currently preferred, this flag is
    42  essentially mandatory.
    43  
    44  Arguments after the filename are interpreted as LSP CodeAction kinds
    45  to be applied; the default set is {"quickfix"}, but valid kinds include:
    46  
    47  	quickfix
    48  	refactor
    49  	refactor.extract
    50  	refactor.inline
    51  	refactor.rewrite
    52  	source.organizeImports
    53  	source.fixAll
    54  
    55  CodeAction kinds are hierarchical, so "refactor" includes
    56  "refactor.inline". There is currently no way to enable or even
    57  enumerate all kinds.
    58  
    59  Example: apply any "refactor.rewrite" fixes at the specific byte
    60  offset within this file:
    61  
    62  	$ gopls fix -a internal/cmd/check.go:#43 refactor.rewrite
    63  
    64  fix-flags:
    65  `)
    66  	printFlagDefaults(f)
    67  }
    68  
    69  // Run performs diagnostic checks on the file specified and either;
    70  // - if -w is specified, updates the file in place;
    71  // - if -d is specified, prints out unified diffs of the changes; or
    72  // - otherwise, prints the new versions to stdout.
    73  func (s *suggestedFix) Run(ctx context.Context, args ...string) error {
    74  	if len(args) < 1 {
    75  		return tool.CommandLineErrorf("fix expects at least 1 argument")
    76  	}
    77  	s.app.editFlags = &s.EditFlags
    78  	conn, err := s.app.connect(ctx, nil)
    79  	if err != nil {
    80  		return err
    81  	}
    82  	defer conn.terminate(ctx)
    83  
    84  	from := parseSpan(args[0])
    85  	uri := from.URI()
    86  	file, err := conn.openFile(ctx, uri)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	rng, err := file.spanRange(from)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// Get diagnostics.
    96  	if err := conn.diagnoseFiles(ctx, []protocol.DocumentURI{uri}); err != nil {
    97  		return err
    98  	}
    99  	diagnostics := []protocol.Diagnostic{} // LSP wants non-nil slice
   100  	conn.client.filesMu.Lock()
   101  	diagnostics = append(diagnostics, file.diagnostics...)
   102  	conn.client.filesMu.Unlock()
   103  
   104  	// Request code actions
   105  	codeActionKinds := []protocol.CodeActionKind{protocol.QuickFix}
   106  	if len(args) > 1 {
   107  		codeActionKinds = []protocol.CodeActionKind{}
   108  		for _, k := range args[1:] {
   109  			codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k))
   110  		}
   111  	}
   112  	p := protocol.CodeActionParams{
   113  		TextDocument: protocol.TextDocumentIdentifier{
   114  			URI: uri,
   115  		},
   116  		Context: protocol.CodeActionContext{
   117  			Only:        codeActionKinds,
   118  			Diagnostics: diagnostics,
   119  		},
   120  		Range: rng,
   121  	}
   122  	actions, err := conn.CodeAction(ctx, &p)
   123  	if err != nil {
   124  		return fmt.Errorf("%v: %v", from, err)
   125  	}
   126  
   127  	// Gather edits from matching code actions.
   128  	var edits []protocol.TextEdit
   129  	for _, a := range actions {
   130  		// Without -all, apply only "preferred" fixes.
   131  		if !a.IsPreferred && !s.All {
   132  			continue
   133  		}
   134  
   135  		// Execute any command.
   136  		// This may cause the server to make
   137  		// an ApplyEdit downcall to the client.
   138  		if a.Command != nil {
   139  			if _, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
   140  				Command:   a.Command.Command,
   141  				Arguments: a.Command.Arguments,
   142  			}); err != nil {
   143  				return err
   144  			}
   145  			// The specification says that commands should
   146  			// be executed _after_ edits are applied, not
   147  			// instead of them, but we don't want to
   148  			// duplicate edits.
   149  			continue
   150  		}
   151  
   152  		// If the provided span has a position (not just offsets),
   153  		// and the action has diagnostics, the action must have a
   154  		// diagnostic with the same range as it.
   155  		if from.HasPosition() && len(a.Diagnostics) > 0 &&
   156  			!slices.ContainsFunc(a.Diagnostics, func(diag protocol.Diagnostic) bool {
   157  				return diag.Range.Start == rng.Start
   158  			}) {
   159  			continue
   160  		}
   161  
   162  		// Partially apply CodeAction.Edit, a WorkspaceEdit.
   163  		// (See also conn.Client.applyWorkspaceEdit(a.Edit)).
   164  		for _, c := range a.Edit.DocumentChanges {
   165  			tde := c.TextDocumentEdit
   166  			if tde != nil && tde.TextDocument.URI == uri {
   167  				edits = append(edits, protocol.AsTextEdits(tde.Edits)...)
   168  			}
   169  		}
   170  	}
   171  
   172  	return applyTextEdits(file.mapper, edits, s.app.editFlags)
   173  }