github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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/powerman/golang-tools/go/types/typeutil"
    18  	"github.com/powerman/golang-tools/internal/event"
    19  	"github.com/powerman/golang-tools/internal/lsp/diff"
    20  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    21  	"github.com/powerman/golang-tools/internal/span"
    22  	"github.com/powerman/golang-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 dep 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  // PrepareRename searches for a valid renaming at position pp.
    46  //
    47  // The returned usererr is intended to be displayed to the user to explain why
    48  // the prepare fails. Probably we could eliminate the redundancy in returning
    49  // two errors, but for now this is done defensively.
    50  func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) {
    51  	ctx, done := event.Start(ctx, "source.PrepareRename")
    52  	defer done()
    53  
    54  	qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp)
    55  	if err != nil {
    56  		return nil, nil, err
    57  	}
    58  	node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg
    59  	if err := checkRenamable(obj); err != nil {
    60  		return nil, err, err
    61  	}
    62  	mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End())
    63  	if err != nil {
    64  		return nil, nil, err
    65  	}
    66  	rng, err := mr.Range()
    67  	if err != nil {
    68  		return nil, nil, err
    69  	}
    70  	if _, isImport := node.(*ast.ImportSpec); isImport {
    71  		// We're not really renaming the import path.
    72  		rng.End = rng.Start
    73  	}
    74  	return &PrepareItem{
    75  		Range: rng,
    76  		Text:  obj.Name(),
    77  	}, nil, nil
    78  }
    79  
    80  // checkRenamable verifies if an obj may be renamed.
    81  func checkRenamable(obj types.Object) error {
    82  	if v, ok := obj.(*types.Var); ok && v.Embedded() {
    83  		return errors.New("can't rename embedded fields: rename the type directly or name the field")
    84  	}
    85  	if obj.Name() == "_" {
    86  		return errors.New("can't rename \"_\"")
    87  	}
    88  	return nil
    89  }
    90  
    91  // Rename returns a map of TextEdits for each file modified when renaming a
    92  // given identifier within a package.
    93  func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) {
    94  	ctx, done := event.Start(ctx, "source.Rename")
    95  	defer done()
    96  
    97  	qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	obj, pkg := qos[0].obj, qos[0].pkg
   103  
   104  	if err := checkRenamable(obj); err != nil {
   105  		return nil, err
   106  	}
   107  	if obj.Name() == newName {
   108  		return nil, errors.Errorf("old and new names are the same: %s", newName)
   109  	}
   110  	if !isValidIdentifier(newName) {
   111  		return nil, errors.Errorf("invalid identifier to rename: %q", newName)
   112  	}
   113  	if pkg == nil || pkg.IsIllTyped() {
   114  		return nil, errors.Errorf("package for %s is ill typed", f.URI())
   115  	}
   116  	refs, err := references(ctx, s, qos, true, false, true)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	r := renamer{
   121  		ctx:          ctx,
   122  		fset:         s.FileSet(),
   123  		refs:         refs,
   124  		objsToUpdate: make(map[types.Object]bool),
   125  		from:         obj.Name(),
   126  		to:           newName,
   127  		packages:     make(map[*types.Package]Package),
   128  	}
   129  
   130  	// A renaming initiated at an interface method indicates the
   131  	// intention to rename abstract and concrete methods as needed
   132  	// to preserve assignability.
   133  	for _, ref := range refs {
   134  		if obj, ok := ref.obj.(*types.Func); ok {
   135  			recv := obj.Type().(*types.Signature).Recv()
   136  			if recv != nil && IsInterface(recv.Type().Underlying()) {
   137  				r.changeMethods = true
   138  				break
   139  			}
   140  		}
   141  	}
   142  	for _, from := range refs {
   143  		r.packages[from.pkg.GetTypes()] = from.pkg
   144  	}
   145  
   146  	// Check that the renaming of the identifier is ok.
   147  	for _, ref := range refs {
   148  		r.check(ref.obj)
   149  		if r.hadConflicts { // one error is enough.
   150  			break
   151  		}
   152  	}
   153  	if r.hadConflicts {
   154  		return nil, errors.Errorf(r.errors)
   155  	}
   156  
   157  	changes, err := r.update()
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	result := make(map[span.URI][]protocol.TextEdit)
   162  	for uri, edits := range changes {
   163  		// These edits should really be associated with FileHandles for maximal correctness.
   164  		// For now, this is good enough.
   165  		fh, err := s.GetFile(ctx, uri)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  		data, err := fh.Read()
   170  		if err != nil {
   171  			return nil, err
   172  		}
   173  		converter := span.NewContentConverter(uri.Filename(), data)
   174  		m := &protocol.ColumnMapper{
   175  			URI:       uri,
   176  			Converter: converter,
   177  			Content:   data,
   178  		}
   179  		// Sort the edits first.
   180  		diff.SortTextEdits(edits)
   181  		protocolEdits, err := ToProtocolEdits(m, edits)
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  		result[uri] = protocolEdits
   186  	}
   187  	return result, nil
   188  }
   189  
   190  // Rename all references to the identifier.
   191  func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) {
   192  	result := make(map[span.URI][]diff.TextEdit)
   193  	seen := make(map[span.Span]bool)
   194  
   195  	docRegexp, err := regexp.Compile(`\b` + r.from + `\b`)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	for _, ref := range r.refs {
   200  		refSpan, err := ref.spanRange.Span()
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  		if seen[refSpan] {
   205  			continue
   206  		}
   207  		seen[refSpan] = true
   208  
   209  		// Renaming a types.PkgName may result in the addition or removal of an identifier,
   210  		// so we deal with this separately.
   211  		if pkgName, ok := ref.obj.(*types.PkgName); ok && ref.isDeclaration {
   212  			edit, err := r.updatePkgName(pkgName)
   213  			if err != nil {
   214  				return nil, err
   215  			}
   216  			result[refSpan.URI()] = append(result[refSpan.URI()], *edit)
   217  			continue
   218  		}
   219  
   220  		// Replace the identifier with r.to.
   221  		edit := diff.TextEdit{
   222  			Span:    refSpan,
   223  			NewText: r.to,
   224  		}
   225  
   226  		result[refSpan.URI()] = append(result[refSpan.URI()], edit)
   227  
   228  		if !ref.isDeclaration || ref.ident == nil { // uses do not have doc comments to update.
   229  			continue
   230  		}
   231  
   232  		doc := r.docComment(ref.pkg, ref.ident)
   233  		if doc == nil {
   234  			continue
   235  		}
   236  
   237  		// Perform the rename in doc comments declared in the original package.
   238  		// go/parser strips out \r\n returns from the comment text, so go
   239  		// line-by-line through the comment text to get the correct positions.
   240  		for _, comment := range doc.List {
   241  			if isDirective(comment.Text) {
   242  				continue
   243  			}
   244  			lines := strings.Split(comment.Text, "\n")
   245  			tok := r.fset.File(comment.Pos())
   246  			commentLine := tok.Position(comment.Pos()).Line
   247  			for i, line := range lines {
   248  				lineStart := comment.Pos()
   249  				if i > 0 {
   250  					lineStart = tok.LineStart(commentLine + i)
   251  				}
   252  				for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) {
   253  					rng := span.NewRange(r.fset, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]))
   254  					spn, err := rng.Span()
   255  					if err != nil {
   256  						return nil, err
   257  					}
   258  					result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
   259  						Span:    spn,
   260  						NewText: r.to,
   261  					})
   262  				}
   263  			}
   264  		}
   265  	}
   266  
   267  	return result, nil
   268  }
   269  
   270  // docComment returns the doc for an identifier.
   271  func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup {
   272  	_, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End())
   273  	for _, node := range nodes {
   274  		switch decl := node.(type) {
   275  		case *ast.FuncDecl:
   276  			return decl.Doc
   277  		case *ast.Field:
   278  			return decl.Doc
   279  		case *ast.GenDecl:
   280  			return decl.Doc
   281  		// For {Type,Value}Spec, if the doc on the spec is absent,
   282  		// search for the enclosing GenDecl
   283  		case *ast.TypeSpec:
   284  			if decl.Doc != nil {
   285  				return decl.Doc
   286  			}
   287  		case *ast.ValueSpec:
   288  			if decl.Doc != nil {
   289  				return decl.Doc
   290  			}
   291  		case *ast.Ident:
   292  		case *ast.AssignStmt:
   293  			// *ast.AssignStmt doesn't have an associated comment group.
   294  			// So, we try to find a comment just before the identifier.
   295  
   296  			// Try to find a comment group only for short variable declarations (:=).
   297  			if decl.Tok != token.DEFINE {
   298  				return nil
   299  			}
   300  
   301  			var file *ast.File
   302  			for _, f := range pkg.GetSyntax() {
   303  				if f.Pos() <= id.Pos() && id.Pos() <= f.End() {
   304  					file = f
   305  					break
   306  				}
   307  			}
   308  			if file == nil {
   309  				return nil
   310  			}
   311  
   312  			identLine := r.fset.Position(id.Pos()).Line
   313  			for _, comment := range file.Comments {
   314  				if comment.Pos() > id.Pos() {
   315  					// Comment is after the identifier.
   316  					continue
   317  				}
   318  
   319  				lastCommentLine := r.fset.Position(comment.End()).Line
   320  				if lastCommentLine+1 == identLine {
   321  					return comment
   322  				}
   323  			}
   324  		default:
   325  			return nil
   326  		}
   327  	}
   328  	return nil
   329  }
   330  
   331  // updatePkgName returns the updates to rename a pkgName in the import spec
   332  func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) {
   333  	// Modify ImportSpec syntax to add or remove the Name as needed.
   334  	pkg := r.packages[pkgName.Pkg()]
   335  	_, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos())
   336  	if len(path) < 2 {
   337  		return nil, errors.Errorf("no path enclosing interval for %s", pkgName.Name())
   338  	}
   339  	spec, ok := path[1].(*ast.ImportSpec)
   340  	if !ok {
   341  		return nil, errors.Errorf("failed to update PkgName for %s", pkgName.Name())
   342  	}
   343  
   344  	var astIdent *ast.Ident // will be nil if ident is removed
   345  	if pkgName.Imported().Name() != r.to {
   346  		// ImportSpec.Name needed
   347  		astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to}
   348  	}
   349  
   350  	// Make a copy of the ident that just has the name and path.
   351  	updated := &ast.ImportSpec{
   352  		Name:   astIdent,
   353  		Path:   spec.Path,
   354  		EndPos: spec.EndPos,
   355  	}
   356  
   357  	rng := span.NewRange(r.fset, spec.Pos(), spec.End())
   358  	spn, err := rng.Span()
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	var buf bytes.Buffer
   364  	format.Node(&buf, r.fset, updated)
   365  	newText := buf.String()
   366  
   367  	return &diff.TextEdit{
   368  		Span:    spn,
   369  		NewText: newText,
   370  	}, nil
   371  }