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

     1  // Copyright 2020 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  	"fmt"
    10  	"go/ast"
    11  	"go/token"
    12  	"go/types"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/gopls/internal/analysis/embeddirective"
    16  	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
    17  	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
    18  	"golang.org/x/tools/gopls/internal/analysis/undeclaredname"
    19  	"golang.org/x/tools/gopls/internal/analysis/unusedparams"
    20  	"golang.org/x/tools/gopls/internal/cache"
    21  	"golang.org/x/tools/gopls/internal/cache/parsego"
    22  	"golang.org/x/tools/gopls/internal/file"
    23  	"golang.org/x/tools/gopls/internal/protocol"
    24  	"golang.org/x/tools/gopls/internal/util/bug"
    25  	"golang.org/x/tools/internal/imports"
    26  )
    27  
    28  // A fixer is a function that suggests a fix for a diagnostic produced
    29  // by the analysis framework. This is done outside of the analyzer Run
    30  // function so that the construction of expensive fixes can be
    31  // deferred until they are requested by the user.
    32  //
    33  // The actual diagnostic is not provided; only its position, as the
    34  // triple (pgf, start, end); the resulting SuggestedFix implicitly
    35  // relates to that file.
    36  //
    37  // The supplied token positions (start, end) must belong to
    38  // pkg.FileSet(), and the returned positions
    39  // (SuggestedFix.TextEdits[*].{Pos,End}) must belong to the returned
    40  // FileSet.
    41  //
    42  // A fixer may return (nil, nil) if no fix is available.
    43  type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error)
    44  
    45  // A singleFileFixer is a Fixer that inspects only a single file,
    46  // and does not depend on data types from the cache package.
    47  //
    48  // TODO(adonovan): move fillstruct and undeclaredname into this
    49  // package, so we can remove the import restriction and push
    50  // the singleFile wrapper down into each singleFileFixer?
    51  type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error)
    52  
    53  // singleFile adapts a single-file fixer to a Fixer.
    54  func singleFile(fixer1 singleFileFixer) fixer {
    55  	return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
    56  		return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo())
    57  	}
    58  }
    59  
    60  // Names of ApplyFix.Fix created directly by the CodeAction handler.
    61  const (
    62  	fixExtractVariable   = "extract_variable"
    63  	fixExtractFunction   = "extract_function"
    64  	fixExtractMethod     = "extract_method"
    65  	fixInlineCall        = "inline_call"
    66  	fixInvertIfCondition = "invert_if_condition"
    67  )
    68  
    69  // ApplyFix applies the specified kind of suggested fix to the given
    70  // file and range, returning the resulting edits.
    71  //
    72  // A fix kind is either the Category of an analysis.Diagnostic that
    73  // had a SuggestedFix with no edits; or the name of a fix agreed upon
    74  // by [CodeActions] and this function.
    75  // Fix kinds identify fixes in the command protocol.
    76  //
    77  // TODO(adonovan): come up with a better mechanism for registering the
    78  // connection between analyzers, code actions, and fixers. A flaw of
    79  // the current approach is that the same Category could in theory
    80  // apply to a Diagnostic with several lazy fixes, making them
    81  // impossible to distinguish. It would more precise if there was a
    82  // SuggestedFix.Category field, or some other way to squirrel metadata
    83  // in the fix.
    84  func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
    85  	// This can't be expressed as an entry in the fixer table below
    86  	// because it operates in the protocol (not go/{token,ast}) domain.
    87  	// (Sigh; perhaps it was a mistake to factor out the
    88  	// NarrowestPackageForFile/RangePos/suggestedFixToEdits
    89  	// steps.)
    90  	if fix == unusedparams.FixCategory {
    91  		changes, err := RemoveUnusedParameter(ctx, fh, rng, snapshot)
    92  		if err != nil {
    93  			return nil, err
    94  		}
    95  		// Unwrap TextDocumentEdits again!
    96  		var edits []protocol.TextDocumentEdit
    97  		for _, change := range changes {
    98  			edits = append(edits, *change.TextDocumentEdit)
    99  		}
   100  		return edits, nil
   101  	}
   102  
   103  	fixers := map[string]fixer{
   104  		// Fixes for analyzer-provided diagnostics.
   105  		// These match the Diagnostic.Category.
   106  		embeddirective.FixCategory: addEmbedImport,
   107  		fillstruct.FixCategory:     singleFile(fillstruct.SuggestedFix),
   108  		stubmethods.FixCategory:    stubMethodsFixer,
   109  		undeclaredname.FixCategory: singleFile(undeclaredname.SuggestedFix),
   110  
   111  		// Ad-hoc fixers: these are used when the command is
   112  		// constructed directly by logic in server/code_action.
   113  		fixExtractFunction:   singleFile(extractFunction),
   114  		fixExtractMethod:     singleFile(extractMethod),
   115  		fixExtractVariable:   singleFile(extractVariable),
   116  		fixInlineCall:        inlineCall,
   117  		fixInvertIfCondition: singleFile(invertIfCondition),
   118  	}
   119  	fixer, ok := fixers[fix]
   120  	if !ok {
   121  		return nil, fmt.Errorf("no suggested fix function for %s", fix)
   122  	}
   123  	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	start, end, err := pgf.RangePos(rng)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	fixFset, suggestion, err := fixer(ctx, snapshot, pkg, pgf, start, end)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	if suggestion == nil {
   136  		return nil, nil
   137  	}
   138  	return suggestedFixToEdits(ctx, snapshot, fixFset, suggestion)
   139  }
   140  
   141  // suggestedFixToEdits converts the suggestion's edits from analysis form into protocol form.
   142  func suggestedFixToEdits(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, suggestion *analysis.SuggestedFix) ([]protocol.TextDocumentEdit, error) {
   143  	editsPerFile := map[protocol.DocumentURI]*protocol.TextDocumentEdit{}
   144  	for _, edit := range suggestion.TextEdits {
   145  		tokFile := fset.File(edit.Pos)
   146  		if tokFile == nil {
   147  			return nil, bug.Errorf("no file for edit position")
   148  		}
   149  		end := edit.End
   150  		if !end.IsValid() {
   151  			end = edit.Pos
   152  		}
   153  		fh, err := snapshot.ReadFile(ctx, protocol.URIFromPath(tokFile.Name()))
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  		te, ok := editsPerFile[fh.URI()]
   158  		if !ok {
   159  			te = &protocol.TextDocumentEdit{
   160  				TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
   161  					Version: fh.Version(),
   162  					TextDocumentIdentifier: protocol.TextDocumentIdentifier{
   163  						URI: fh.URI(),
   164  					},
   165  				},
   166  			}
   167  			editsPerFile[fh.URI()] = te
   168  		}
   169  		content, err := fh.Content()
   170  		if err != nil {
   171  			return nil, err
   172  		}
   173  		m := protocol.NewMapper(fh.URI(), content) // TODO(adonovan): opt: memoize in map
   174  		rng, err := m.PosRange(tokFile, edit.Pos, end)
   175  		if err != nil {
   176  			return nil, err
   177  		}
   178  		te.Edits = append(te.Edits, protocol.Or_TextDocumentEdit_edits_Elem{
   179  			Value: protocol.TextEdit{
   180  				Range:   rng,
   181  				NewText: string(edit.NewText),
   182  			},
   183  		})
   184  	}
   185  	var edits []protocol.TextDocumentEdit
   186  	for _, edit := range editsPerFile {
   187  		edits = append(edits, *edit)
   188  	}
   189  	return edits, nil
   190  }
   191  
   192  // addEmbedImport adds a missing embed "embed" import with blank name.
   193  func addEmbedImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, _, _ token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
   194  	// Like golang.AddImport, but with _ as Name and using our pgf.
   195  	protoEdits, err := ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{
   196  		StmtInfo: imports.ImportInfo{
   197  			ImportPath: "embed",
   198  			Name:       "_",
   199  		},
   200  		FixType: imports.AddImport,
   201  	})
   202  	if err != nil {
   203  		return nil, nil, fmt.Errorf("compute edits: %w", err)
   204  	}
   205  
   206  	var edits []analysis.TextEdit
   207  	for _, e := range protoEdits {
   208  		start, end, err := pgf.RangePos(e.Range)
   209  		if err != nil {
   210  			return nil, nil, err // e.g. invalid range
   211  		}
   212  		edits = append(edits, analysis.TextEdit{
   213  			Pos:     start,
   214  			End:     end,
   215  			NewText: []byte(e.NewText),
   216  		})
   217  	}
   218  
   219  	return pkg.FileSet(), &analysis.SuggestedFix{
   220  		Message:   "Add embed import",
   221  		TextEdits: edits,
   222  	}, nil
   223  }