+++ /dev/null
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package source
-
-import (
- "bytes"
- "context"
- "go/ast"
- "go/format"
- "go/token"
- "go/types"
- "regexp"
- "strings"
-
- "golang.org/x/tools/go/types/typeutil"
- "golang.org/x/tools/internal/event"
- "golang.org/x/tools/internal/lsp/diff"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/span"
- "golang.org/x/tools/refactor/satisfy"
- errors "golang.org/x/xerrors"
-)
-
-type renamer struct {
- ctx context.Context
- fset *token.FileSet
- refs []*ReferenceInfo
- objsToUpdate map[types.Object]bool
- hadConflicts bool
- errors string
- from, to string
- satisfyConstraints map[satisfy.Constraint]bool
- packages map[*types.Package]Package // may include additional packages that are a rdep of pkg
- msets typeutil.MethodSetCache
- changeMethods bool
-}
-
-type PrepareItem struct {
- Range protocol.Range
- Text string
-}
-
-func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (*PrepareItem, error) {
- ctx, done := event.Start(ctx, "source.PrepareRename")
- defer done()
-
- qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f, pp)
- if err != nil {
- return nil, err
- }
- node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg
- mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End())
- if err != nil {
- return nil, err
- }
- rng, err := mr.Range()
- if err != nil {
- return nil, err
- }
- if _, isImport := node.(*ast.ImportSpec); isImport {
- // We're not really renaming the import path.
- rng.End = rng.Start
- }
- return &PrepareItem{
- Range: rng,
- Text: obj.Name(),
- }, nil
-}
-
-// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
-func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) {
- ctx, done := event.Start(ctx, "source.Rename")
- defer done()
-
- qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
- if err != nil {
- return nil, err
- }
-
- obj := qos[0].obj
- pkg := qos[0].pkg
-
- if obj.Name() == newName {
- return nil, errors.Errorf("old and new names are the same: %s", newName)
- }
- if !isValidIdentifier(newName) {
- return nil, errors.Errorf("invalid identifier to rename: %q", newName)
- }
- if pkg == nil || pkg.IsIllTyped() {
- return nil, errors.Errorf("package for %s is ill typed", f.URI())
- }
- refs, err := references(ctx, s, qos, true, false)
- if err != nil {
- return nil, err
- }
- r := renamer{
- ctx: ctx,
- fset: s.FileSet(),
- refs: refs,
- objsToUpdate: make(map[types.Object]bool),
- from: obj.Name(),
- to: newName,
- packages: make(map[*types.Package]Package),
- }
-
- // A renaming initiated at an interface method indicates the
- // intention to rename abstract and concrete methods as needed
- // to preserve assignability.
- for _, ref := range refs {
- if obj, ok := ref.obj.(*types.Func); ok {
- recv := obj.Type().(*types.Signature).Recv()
- if recv != nil && IsInterface(recv.Type().Underlying()) {
- r.changeMethods = true
- break
- }
- }
- }
- for _, from := range refs {
- r.packages[from.pkg.GetTypes()] = from.pkg
- }
-
- // Check that the renaming of the identifier is ok.
- for _, ref := range refs {
- r.check(ref.obj)
- if r.hadConflicts { // one error is enough.
- break
- }
- }
- if r.hadConflicts {
- return nil, errors.Errorf(r.errors)
- }
-
- changes, err := r.update()
- if err != nil {
- return nil, err
- }
- result := make(map[span.URI][]protocol.TextEdit)
- for uri, edits := range changes {
- // These edits should really be associated with FileHandles for maximal correctness.
- // For now, this is good enough.
- fh, err := s.GetFile(ctx, uri)
- if err != nil {
- return nil, err
- }
- data, err := fh.Read()
- if err != nil {
- return nil, err
- }
- converter := span.NewContentConverter(uri.Filename(), data)
- m := &protocol.ColumnMapper{
- URI: uri,
- Converter: converter,
- Content: data,
- }
- // Sort the edits first.
- diff.SortTextEdits(edits)
- protocolEdits, err := ToProtocolEdits(m, edits)
- if err != nil {
- return nil, err
- }
- result[uri] = protocolEdits
- }
- return result, nil
-}
-
-// Rename all references to the identifier.
-func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) {
- result := make(map[span.URI][]diff.TextEdit)
- seen := make(map[span.Span]bool)
-
- docRegexp, err := regexp.Compile(`\b` + r.from + `\b`)
- if err != nil {
- return nil, err
- }
- for _, ref := range r.refs {
- refSpan, err := ref.spanRange.Span()
- if err != nil {
- return nil, err
- }
- if seen[refSpan] {
- continue
- }
- seen[refSpan] = true
-
- // Renaming a types.PkgName may result in the addition or removal of an identifier,
- // so we deal with this separately.
- if pkgName, ok := ref.obj.(*types.PkgName); ok && ref.isDeclaration {
- edit, err := r.updatePkgName(pkgName)
- if err != nil {
- return nil, err
- }
- result[refSpan.URI()] = append(result[refSpan.URI()], *edit)
- continue
- }
-
- // Replace the identifier with r.to.
- edit := diff.TextEdit{
- Span: refSpan,
- NewText: r.to,
- }
-
- result[refSpan.URI()] = append(result[refSpan.URI()], edit)
-
- if !ref.isDeclaration || ref.ident == nil { // uses do not have doc comments to update.
- continue
- }
-
- doc := r.docComment(ref.pkg, ref.ident)
- if doc == nil {
- continue
- }
-
- // Perform the rename in doc comments declared in the original package.
- // go/parser strips out \r\n returns from the comment text, so go
- // line-by-line through the comment text to get the correct positions.
- for _, comment := range doc.List {
- if isDirective(comment.Text) {
- continue
- }
- lines := strings.Split(comment.Text, "\n")
- tok := r.fset.File(comment.Pos())
- commentLine := tok.Position(comment.Pos()).Line
- for i, line := range lines {
- lineStart := comment.Pos()
- if i > 0 {
- lineStart = tok.LineStart(commentLine + i)
- }
- for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) {
- rng := span.NewRange(r.fset, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]))
- spn, err := rng.Span()
- if err != nil {
- return nil, err
- }
- result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
- Span: spn,
- NewText: r.to,
- })
- }
- }
- }
- }
-
- return result, nil
-}
-
-// docComment returns the doc for an identifier.
-func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup {
- _, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End())
- for _, node := range nodes {
- switch decl := node.(type) {
- case *ast.FuncDecl:
- return decl.Doc
- case *ast.Field:
- return decl.Doc
- case *ast.GenDecl:
- return decl.Doc
- // For {Type,Value}Spec, if the doc on the spec is absent,
- // search for the enclosing GenDecl
- case *ast.TypeSpec:
- if decl.Doc != nil {
- return decl.Doc
- }
- case *ast.ValueSpec:
- if decl.Doc != nil {
- return decl.Doc
- }
- case *ast.Ident:
- default:
- return nil
- }
- }
- return nil
-}
-
-// updatePkgName returns the updates to rename a pkgName in the import spec
-func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) {
- // Modify ImportSpec syntax to add or remove the Name as needed.
- pkg := r.packages[pkgName.Pkg()]
- _, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos())
- if len(path) < 2 {
- return nil, errors.Errorf("no path enclosing interval for %s", pkgName.Name())
- }
- spec, ok := path[1].(*ast.ImportSpec)
- if !ok {
- return nil, errors.Errorf("failed to update PkgName for %s", pkgName.Name())
- }
-
- var astIdent *ast.Ident // will be nil if ident is removed
- if pkgName.Imported().Name() != r.to {
- // ImportSpec.Name needed
- astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to}
- }
-
- // Make a copy of the ident that just has the name and path.
- updated := &ast.ImportSpec{
- Name: astIdent,
- Path: spec.Path,
- EndPos: spec.EndPos,
- }
-
- rng := span.NewRange(r.fset, spec.Pos(), spec.End())
- spn, err := rng.Span()
- if err != nil {
- return nil, err
- }
-
- var buf bytes.Buffer
- format.Node(&buf, r.fset, updated)
- newText := buf.String()
-
- return &diff.TextEdit{
- Span: spn,
- NewText: newText,
- }, nil
-}