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
5package source
6
7import (
8	"bytes"
9	"context"
10	"go/ast"
11	"go/format"
12	"go/token"
13	"go/types"
14	"regexp"
15
16	"golang.org/x/tools/go/types/typeutil"
17	"golang.org/x/tools/internal/lsp/diff"
18	"golang.org/x/tools/internal/lsp/protocol"
19	"golang.org/x/tools/internal/span"
20	"golang.org/x/tools/internal/telemetry/trace"
21	"golang.org/x/tools/refactor/satisfy"
22	errors "golang.org/x/xerrors"
23)
24
25type renamer struct {
26	ctx                context.Context
27	fset               *token.FileSet
28	refs               []*ReferenceInfo
29	objsToUpdate       map[types.Object]bool
30	hadConflicts       bool
31	errors             string
32	from, to           string
33	satisfyConstraints map[satisfy.Constraint]bool
34	packages           map[*types.Package]Package // may include additional packages that are a rdep of pkg
35	msets              typeutil.MethodSetCache
36	changeMethods      bool
37}
38
39type PrepareItem struct {
40	Range protocol.Range
41	Text  string
42}
43
44func PrepareRename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) (*PrepareItem, error) {
45	ctx, done := trace.StartSpan(ctx, "source.PrepareRename")
46	defer done()
47
48	qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
49	if err != nil {
50		return nil, err
51	}
52
53	// Do not rename builtin identifiers.
54	if qos[0].obj.Parent() == types.Universe {
55		return nil, errors.Errorf("cannot rename builtin %q", qos[0].obj.Name())
56	}
57
58	mr, err := posToMappedRange(s.View(), qos[0].sourcePkg, qos[0].node.Pos(), qos[0].node.End())
59	if err != nil {
60		return nil, err
61	}
62
63	rng, err := mr.Range()
64	if err != nil {
65		return nil, err
66	}
67
68	if _, isImport := qos[0].node.(*ast.ImportSpec); isImport {
69		// We're not really renaming the import path.
70		rng.End = rng.Start
71	}
72
73	return &PrepareItem{
74		Range: rng,
75		Text:  qos[0].obj.Name(),
76	}, nil
77}
78
79// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
80func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) {
81	ctx, done := trace.StartSpan(ctx, "source.Rename")
82	defer done()
83
84	qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
85	if err != nil {
86		return nil, err
87	}
88
89	obj := qos[0].obj
90	pkg := qos[0].pkg
91
92	if obj.Name() == newName {
93		return nil, errors.Errorf("old and new names are the same: %s", newName)
94	}
95	if !isValidIdentifier(newName) {
96		return nil, errors.Errorf("invalid identifier to rename: %q", newName)
97	}
98	// Do not rename builtin identifiers.
99	if obj.Parent() == types.Universe {
100		return nil, errors.Errorf("cannot rename builtin %q", obj.Name())
101	}
102	if pkg == nil || pkg.IsIllTyped() {
103		return nil, errors.Errorf("package for %s is ill typed", f.Identity().URI)
104	}
105
106	refs, err := References(ctx, s, f, pp, true)
107	if err != nil {
108		return nil, err
109	}
110
111	r := renamer{
112		ctx:          ctx,
113		fset:         s.View().Session().Cache().FileSet(),
114		refs:         refs,
115		objsToUpdate: make(map[types.Object]bool),
116		from:         obj.Name(),
117		to:           newName,
118		packages:     make(map[*types.Package]Package),
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(uri)
144		if err != nil {
145			return nil, err
146		}
147		data, _, err := fh.Read(ctx)
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.
169func (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		for _, comment := range doc.List {
217			for _, locs := range docRegexp.FindAllStringIndex(comment.Text, -1) {
218				rng := span.NewRange(r.fset, comment.Pos()+token.Pos(locs[0]), comment.Pos()+token.Pos(locs[1]))
219				spn, err := rng.Span()
220				if err != nil {
221					return nil, err
222				}
223				result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
224					Span:    spn,
225					NewText: r.to,
226				})
227			}
228		}
229	}
230
231	return result, nil
232}
233
234// docComment returns the doc for an identifier.
235func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup {
236	_, nodes, _ := pathEnclosingInterval(r.ctx, r.fset, pkg, id.Pos(), id.End())
237	for _, node := range nodes {
238		switch decl := node.(type) {
239		case *ast.FuncDecl:
240			return decl.Doc
241		case *ast.Field:
242			return decl.Doc
243		case *ast.GenDecl:
244			return decl.Doc
245		// For {Type,Value}Spec, if the doc on the spec is absent,
246		// search for the enclosing GenDecl
247		case *ast.TypeSpec:
248			if decl.Doc != nil {
249				return decl.Doc
250			}
251		case *ast.ValueSpec:
252			if decl.Doc != nil {
253				return decl.Doc
254			}
255		case *ast.Ident:
256		default:
257			return nil
258		}
259	}
260	return nil
261}
262
263// updatePkgName returns the updates to rename a pkgName in the import spec
264func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) {
265	// Modify ImportSpec syntax to add or remove the Name as needed.
266	pkg := r.packages[pkgName.Pkg()]
267	_, path, _ := pathEnclosingInterval(r.ctx, r.fset, pkg, pkgName.Pos(), pkgName.Pos())
268	if len(path) < 2 {
269		return nil, errors.Errorf("no path enclosing interval for %s", pkgName.Name())
270	}
271	spec, ok := path[1].(*ast.ImportSpec)
272	if !ok {
273		return nil, errors.Errorf("failed to update PkgName for %s", pkgName.Name())
274	}
275
276	var astIdent *ast.Ident // will be nil if ident is removed
277	if pkgName.Imported().Name() != r.to {
278		// ImportSpec.Name needed
279		astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to}
280	}
281
282	// Make a copy of the ident that just has the name and path.
283	updated := &ast.ImportSpec{
284		Name:   astIdent,
285		Path:   spec.Path,
286		EndPos: spec.EndPos,
287	}
288
289	rng := span.NewRange(r.fset, spec.Pos(), spec.End())
290	spn, err := rng.Span()
291	if err != nil {
292		return nil, err
293	}
294
295	var buf bytes.Buffer
296	format.Node(&buf, r.fset, updated)
297	newText := buf.String()
298
299	return &diff.TextEdit{
300		Span:    spn,
301		NewText: newText,
302	}, nil
303}
304