github.com/jd-ly/tools@v0.5.7/internal/lsp/source/rename.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 source
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"go/ast"
    11  	"go/format"
    12  	"go/token"
    13  	"go/types"
    14  	"regexp"
    15  	"strings"
    16  
    17  	"github.com/jd-ly/tools/go/types/typeutil"
    18  	"github.com/jd-ly/tools/internal/event"
    19  	"github.com/jd-ly/tools/internal/lsp/diff"
    20  	"github.com/jd-ly/tools/internal/lsp/protocol"
    21  	"github.com/jd-ly/tools/internal/span"
    22  	"github.com/jd-ly/tools/refactor/satisfy"
    23  	errors "golang.org/x/xerrors"
    24  )
    25  
    26  type renamer struct {
    27  	ctx                context.Context
    28  	fset               *token.FileSet
    29  	refs               []*ReferenceInfo
    30  	objsToUpdate       map[types.Object]bool
    31  	hadConflicts       bool
    32  	errors             string
    33  	from, to           string
    34  	satisfyConstraints map[satisfy.Constraint]bool
    35  	packages           map[*types.Package]Package // may include additional packages that are a rdep of pkg
    36  	msets              typeutil.MethodSetCache
    37  	changeMethods      bool
    38  }
    39  
    40  type PrepareItem struct {
    41  	Range protocol.Range
    42  	Text  string
    43  }
    44  
    45  func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (*PrepareItem, error) {
    46  	ctx, done := event.Start(ctx, "source.PrepareRename")
    47  	defer done()
    48  
    49  	qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f, pp)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg
    54  	mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End())
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	rng, err := mr.Range()
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	if _, isImport := node.(*ast.ImportSpec); isImport {
    63  		// We're not really renaming the import path.
    64  		rng.End = rng.Start
    65  	}
    66  	return &PrepareItem{
    67  		Range: rng,
    68  		Text:  obj.Name(),
    69  	}, nil
    70  }
    71  
    72  // Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
    73  func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) {
    74  	ctx, done := event.Start(ctx, "source.Rename")
    75  	defer done()
    76  
    77  	qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	obj := qos[0].obj
    83  	pkg := qos[0].pkg
    84  
    85  	if obj.Name() == newName {
    86  		return nil, errors.Errorf("old and new names are the same: %s", newName)
    87  	}
    88  	if !isValidIdentifier(newName) {
    89  		return nil, errors.Errorf("invalid identifier to rename: %q", newName)
    90  	}
    91  	if pkg == nil || pkg.IsIllTyped() {
    92  		return nil, errors.Errorf("package for %s is ill typed", f.URI())
    93  	}
    94  	refs, err := references(ctx, s, qos, true, false)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	r := renamer{
    99  		ctx:          ctx,
   100  		fset:         s.FileSet(),
   101  		refs:         refs,
   102  		objsToUpdate: make(map[types.Object]bool),
   103  		from:         obj.Name(),
   104  		to:           newName,
   105  		packages:     make(map[*types.Package]Package),
   106  	}
   107  
   108  	// A renaming initiated at an interface method indicates the
   109  	// intention to rename abstract and concrete methods as needed
   110  	// to preserve assignability.
   111  	for _, ref := range refs {
   112  		if obj, ok := ref.obj.(*types.Func); ok {
   113  			recv := obj.Type().(*types.Signature).Recv()
   114  			if recv != nil && IsInterface(recv.Type().Underlying()) {
   115  				r.changeMethods = true
   116  				break
   117  			}
   118  		}
   119  	}
   120  	for _, from := range refs {
   121  		r.packages[from.pkg.GetTypes()] = from.pkg
   122  	}
   123  
   124  	// Check that the renaming of the identifier is ok.
   125  	for _, ref := range refs {
   126  		r.check(ref.obj)
   127  		if r.hadConflicts { // one error is enough.
   128  			break
   129  		}
   130  	}
   131  	if r.hadConflicts {
   132  		return nil, errors.Errorf(r.errors)
   133  	}
   134  
   135  	changes, err := r.update()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	result := make(map[span.URI][]protocol.TextEdit)
   140  	for uri, edits := range changes {
   141  		// These edits should really be associated with FileHandles for maximal correctness.
   142  		// For now, this is good enough.
   143  		fh, err := s.GetFile(ctx, uri)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		data, err := fh.Read()
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  		converter := span.NewContentConverter(uri.Filename(), data)
   152  		m := &protocol.ColumnMapper{
   153  			URI:       uri,
   154  			Converter: converter,
   155  			Content:   data,
   156  		}
   157  		// Sort the edits first.
   158  		diff.SortTextEdits(edits)
   159  		protocolEdits, err := ToProtocolEdits(m, edits)
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  		result[uri] = protocolEdits
   164  	}
   165  	return result, nil
   166  }
   167  
   168  // Rename all references to the identifier.
   169  func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) {
   170  	result := make(map[span.URI][]diff.TextEdit)
   171  	seen := make(map[span.Span]bool)
   172  
   173  	docRegexp, err := regexp.Compile(`\b` + r.from + `\b`)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	for _, ref := range r.refs {
   178  		refSpan, err := ref.spanRange.Span()
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  		if seen[refSpan] {
   183  			continue
   184  		}
   185  		seen[refSpan] = true
   186  
   187  		// Renaming a types.PkgName may result in the addition or removal of an identifier,
   188  		// so we deal with this separately.
   189  		if pkgName, ok := ref.obj.(*types.PkgName); ok && ref.isDeclaration {
   190  			edit, err := r.updatePkgName(pkgName)
   191  			if err != nil {
   192  				return nil, err
   193  			}
   194  			result[refSpan.URI()] = append(result[refSpan.URI()], *edit)
   195  			continue
   196  		}
   197  
   198  		// Replace the identifier with r.to.
   199  		edit := diff.TextEdit{
   200  			Span:    refSpan,
   201  			NewText: r.to,
   202  		}
   203  
   204  		result[refSpan.URI()] = append(result[refSpan.URI()], edit)
   205  
   206  		if !ref.isDeclaration || ref.ident == nil { // uses do not have doc comments to update.
   207  			continue
   208  		}
   209  
   210  		doc := r.docComment(ref.pkg, ref.ident)
   211  		if doc == nil {
   212  			continue
   213  		}
   214  
   215  		// Perform the rename in doc comments declared in the original package.
   216  		// go/parser strips out \r\n returns from the comment text, so go
   217  		// line-by-line through the comment text to get the correct positions.
   218  		for _, comment := range doc.List {
   219  			if isDirective(comment.Text) {
   220  				continue
   221  			}
   222  			lines := strings.Split(comment.Text, "\n")
   223  			tok := r.fset.File(comment.Pos())
   224  			commentLine := tok.Position(comment.Pos()).Line
   225  			for i, line := range lines {
   226  				lineStart := comment.Pos()
   227  				if i > 0 {
   228  					lineStart = tok.LineStart(commentLine + i)
   229  				}
   230  				for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) {
   231  					rng := span.NewRange(r.fset, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]))
   232  					spn, err := rng.Span()
   233  					if err != nil {
   234  						return nil, err
   235  					}
   236  					result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
   237  						Span:    spn,
   238  						NewText: r.to,
   239  					})
   240  				}
   241  			}
   242  		}
   243  	}
   244  
   245  	return result, nil
   246  }
   247  
   248  // docComment returns the doc for an identifier.
   249  func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup {
   250  	_, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End())
   251  	for _, node := range nodes {
   252  		switch decl := node.(type) {
   253  		case *ast.FuncDecl:
   254  			return decl.Doc
   255  		case *ast.Field:
   256  			return decl.Doc
   257  		case *ast.GenDecl:
   258  			return decl.Doc
   259  		// For {Type,Value}Spec, if the doc on the spec is absent,
   260  		// search for the enclosing GenDecl
   261  		case *ast.TypeSpec:
   262  			if decl.Doc != nil {
   263  				return decl.Doc
   264  			}
   265  		case *ast.ValueSpec:
   266  			if decl.Doc != nil {
   267  				return decl.Doc
   268  			}
   269  		case *ast.Ident:
   270  		default:
   271  			return nil
   272  		}
   273  	}
   274  	return nil
   275  }
   276  
   277  // updatePkgName returns the updates to rename a pkgName in the import spec
   278  func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) {
   279  	// Modify ImportSpec syntax to add or remove the Name as needed.
   280  	pkg := r.packages[pkgName.Pkg()]
   281  	_, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos())
   282  	if len(path) < 2 {
   283  		return nil, errors.Errorf("no path enclosing interval for %s", pkgName.Name())
   284  	}
   285  	spec, ok := path[1].(*ast.ImportSpec)
   286  	if !ok {
   287  		return nil, errors.Errorf("failed to update PkgName for %s", pkgName.Name())
   288  	}
   289  
   290  	var astIdent *ast.Ident // will be nil if ident is removed
   291  	if pkgName.Imported().Name() != r.to {
   292  		// ImportSpec.Name needed
   293  		astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to}
   294  	}
   295  
   296  	// Make a copy of the ident that just has the name and path.
   297  	updated := &ast.ImportSpec{
   298  		Name:   astIdent,
   299  		Path:   spec.Path,
   300  		EndPos: spec.EndPos,
   301  	}
   302  
   303  	rng := span.NewRange(r.fset, spec.Pos(), spec.End())
   304  	spn, err := rng.Span()
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  
   309  	var buf bytes.Buffer
   310  	format.Node(&buf, r.fset, updated)
   311  	newText := buf.String()
   312  
   313  	return &diff.TextEdit{
   314  		Span:    spn,
   315  		NewText: newText,
   316  	}, nil
   317  }