github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/source/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 source
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/token"
    12  	"go/types"
    13  
    14  	"github.com/powerman/golang-tools/go/analysis"
    15  	"github.com/powerman/golang-tools/internal/lsp/analysis/fillstruct"
    16  	"github.com/powerman/golang-tools/internal/lsp/analysis/undeclaredname"
    17  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    18  	"github.com/powerman/golang-tools/internal/span"
    19  	errors "golang.org/x/xerrors"
    20  )
    21  
    22  type (
    23  	// SuggestedFixFunc is a function used to get the suggested fixes for a given
    24  	// gopls command, some of which are provided by go/analysis.Analyzers. Some of
    25  	// the analyzers in internal/lsp/analysis are not efficient enough to include
    26  	// suggested fixes with their diagnostics, so we have to compute them
    27  	// separately. Such analyzers should provide a function with a signature of
    28  	// SuggestedFixFunc.
    29  	SuggestedFixFunc  func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*analysis.SuggestedFix, error)
    30  	singleFileFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
    31  )
    32  
    33  const (
    34  	FillStruct      = "fill_struct"
    35  	StubMethods     = "stub_methods"
    36  	UndeclaredName  = "undeclared_name"
    37  	ExtractVariable = "extract_variable"
    38  	ExtractFunction = "extract_function"
    39  	ExtractMethod   = "extract_method"
    40  )
    41  
    42  // suggestedFixes maps a suggested fix command id to its handler.
    43  var suggestedFixes = map[string]SuggestedFixFunc{
    44  	FillStruct:      singleFile(fillstruct.SuggestedFix),
    45  	UndeclaredName:  singleFile(undeclaredname.SuggestedFix),
    46  	ExtractVariable: singleFile(extractVariable),
    47  	ExtractFunction: singleFile(extractFunction),
    48  	ExtractMethod:   singleFile(extractMethod),
    49  	StubMethods:     stubSuggestedFixFunc,
    50  }
    51  
    52  // singleFile calls analyzers that expect inputs for a single file
    53  func singleFile(sf singleFileFixFunc) SuggestedFixFunc {
    54  	return func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*analysis.SuggestedFix, error) {
    55  		fset, rng, src, file, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
    56  		if err != nil {
    57  			return nil, err
    58  		}
    59  		return sf(fset, rng, src, file, pkg, info)
    60  	}
    61  }
    62  
    63  func SuggestedFixFromCommand(cmd protocol.Command, kind protocol.CodeActionKind) SuggestedFix {
    64  	return SuggestedFix{
    65  		Title:      cmd.Title,
    66  		Command:    &cmd,
    67  		ActionKind: kind,
    68  	}
    69  }
    70  
    71  // ApplyFix applies the command's suggested fix to the given file and
    72  // range, returning the resulting edits.
    73  func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
    74  	handler, ok := suggestedFixes[fix]
    75  	if !ok {
    76  		return nil, fmt.Errorf("no suggested fix function for %s", fix)
    77  	}
    78  	suggestion, err := handler(ctx, snapshot, fh, pRng)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	if suggestion == nil {
    83  		return nil, nil
    84  	}
    85  	fset := snapshot.FileSet()
    86  	editsPerFile := map[span.URI]*protocol.TextDocumentEdit{}
    87  	for _, edit := range suggestion.TextEdits {
    88  		spn, err := span.NewRange(fset, edit.Pos, edit.End).Span()
    89  		if err != nil {
    90  			return nil, err
    91  		}
    92  		fh, err := snapshot.GetVersionedFile(ctx, spn.URI())
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		te, ok := editsPerFile[spn.URI()]
    97  		if !ok {
    98  			te = &protocol.TextDocumentEdit{
    99  				TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
   100  					Version: fh.Version(),
   101  					TextDocumentIdentifier: protocol.TextDocumentIdentifier{
   102  						URI: protocol.URIFromSpanURI(fh.URI()),
   103  					},
   104  				},
   105  			}
   106  			editsPerFile[spn.URI()] = te
   107  		}
   108  		_, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  		rng, err := pgf.Mapper.Range(spn)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		te.Edits = append(te.Edits, protocol.TextEdit{
   117  			Range:   rng,
   118  			NewText: string(edit.NewText),
   119  		})
   120  	}
   121  	var edits []protocol.TextDocumentEdit
   122  	for _, edit := range editsPerFile {
   123  		edits = append(edits, *edit)
   124  	}
   125  	return edits, nil
   126  }
   127  
   128  // getAllSuggestedFixInputs is a helper function to collect all possible needed
   129  // inputs for an AppliesFunc or SuggestedFixFunc.
   130  func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *types.Package, *types.Info, error) {
   131  	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
   132  	if err != nil {
   133  		return nil, span.Range{}, nil, nil, nil, nil, errors.Errorf("getting file for Identifier: %w", err)
   134  	}
   135  	rng, err := pgf.Mapper.RangeToSpanRange(pRng)
   136  	if err != nil {
   137  		return nil, span.Range{}, nil, nil, nil, nil, err
   138  	}
   139  	return snapshot.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo(), nil
   140  }