1// Copyright 2014 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 rename contains the implementation of the 'gorename' command
6// whose main function is in golang.org/x/tools/cmd/gorename.
7// See the Usage constant for the command documentation.
8package rename // import "golang.org/x/tools/refactor/rename"
9
10import (
11	"bytes"
12	"errors"
13	"fmt"
14	"go/ast"
15	"go/build"
16	"go/format"
17	"go/parser"
18	"go/token"
19	"go/types"
20	"io"
21	"io/ioutil"
22	"log"
23	"os"
24	"os/exec"
25	"path"
26	"regexp"
27	"sort"
28	"strconv"
29	"strings"
30
31	"golang.org/x/tools/go/loader"
32	"golang.org/x/tools/go/types/typeutil"
33	"golang.org/x/tools/refactor/importgraph"
34	"golang.org/x/tools/refactor/satisfy"
35)
36
37const Usage = `gorename: precise type-safe renaming of identifiers in Go source code.
38
39Usage:
40
41 gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force]
42
43You must specify the object (named entity) to rename using the -offset
44or -from flag.  Exactly one must be specified.
45
46Flags:
47
48-offset    specifies the filename and byte offset of an identifier to rename.
49           This form is intended for use by text editors.
50
51-from      specifies the object to rename using a query notation;
52           This form is intended for interactive use at the command line.
53           A legal -from query has one of the following forms:
54
55  "encoding/json".Decoder.Decode        method of package-level named type
56  (*"encoding/json".Decoder).Decode     ditto, alternative syntax
57  "encoding/json".Decoder.buf           field of package-level named struct type
58  "encoding/json".HTMLEscape            package member (const, func, var, type)
59  "encoding/json".Decoder.Decode::x     local object x within a method
60  "encoding/json".HTMLEscape::x         local object x within a function
61  "encoding/json"::x                    object x anywhere within a package
62  json.go::x                            object x within file json.go
63
64           Double-quotes must be escaped when writing a shell command.
65           Quotes may be omitted for single-segment import paths such as "fmt".
66
67           For methods, the parens and '*' on the receiver type are both
68           optional.
69
70           It is an error if one of the ::x queries matches multiple
71           objects.
72
73-to        the new name.
74
75-force     causes the renaming to proceed even if conflicts were reported.
76           The resulting program may be ill-formed, or experience a change
77           in behaviour.
78
79           WARNING: this flag may even cause the renaming tool to crash.
80           (In due course this bug will be fixed by moving certain
81           analyses into the type-checker.)
82
83-d         display diffs instead of rewriting files
84
85-v         enables verbose logging.
86
87gorename automatically computes the set of packages that might be
88affected.  For a local renaming, this is just the package specified by
89-from or -offset, but for a potentially exported name, gorename scans
90the workspace ($GOROOT and $GOPATH).
91
92gorename rejects renamings of concrete methods that would change the
93assignability relation between types and interfaces.  If the interface
94change was intentional, initiate the renaming at the interface method.
95
96gorename rejects any renaming that would create a conflict at the point
97of declaration, or a reference conflict (ambiguity or shadowing), or
98anything else that could cause the resulting program not to compile.
99
100
101Examples:
102
103$ gorename -offset file.go:#123 -to foo
104
105  Rename the object whose identifier is at byte offset 123 within file file.go.
106
107$ gorename -from '"bytes".Buffer.Len' -to Size
108
109  Rename the "Len" method of the *bytes.Buffer type to "Size".
110`
111
112// ---- TODO ----
113
114// Correctness:
115// - handle dot imports correctly
116// - document limitations (reflection, 'implements' algorithm).
117// - sketch a proof of exhaustiveness.
118
119// Features:
120// - support running on packages specified as *.go files on the command line
121// - support running on programs containing errors (loader.Config.AllowErrors)
122// - allow users to specify a scope other than "global" (to avoid being
123//   stuck by neglected packages in $GOPATH that don't build).
124// - support renaming the package clause (no object)
125// - support renaming an import path (no ident or object)
126//   (requires filesystem + SCM updates).
127// - detect and reject edits to autogenerated files (cgo, protobufs)
128//   and optionally $GOROOT packages.
129// - report all conflicts, or at least all qualitatively distinct ones.
130//   Sometimes we stop to avoid redundancy, but
131//   it may give a disproportionate sense of safety in -force mode.
132// - support renaming all instances of a pattern, e.g.
133//   all receiver vars of a given type,
134//   all local variables of a given type,
135//   all PkgNames for a given package.
136// - emit JSON output for other editors and tools.
137
138var (
139	// Force enables patching of the source files even if conflicts were reported.
140	// The resulting program may be ill-formed.
141	// It may even cause gorename to crash.  TODO(adonovan): fix that.
142	Force bool
143
144	// Diff causes the tool to display diffs instead of rewriting files.
145	Diff bool
146
147	// DiffCmd specifies the diff command used by the -d feature.
148	// (The command must accept a -u flag and two filename arguments.)
149	DiffCmd = "diff"
150
151	// ConflictError is returned by Main when it aborts the renaming due to conflicts.
152	// (It is distinguished because the interesting errors are the conflicts themselves.)
153	ConflictError = errors.New("renaming aborted due to conflicts")
154
155	// Verbose enables extra logging.
156	Verbose bool
157)
158
159var stdout io.Writer = os.Stdout
160
161type renamer struct {
162	iprog              *loader.Program
163	objsToUpdate       map[types.Object]bool
164	hadConflicts       bool
165	from, to           string
166	satisfyConstraints map[satisfy.Constraint]bool
167	packages           map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect
168	msets              typeutil.MethodSetCache
169	changeMethods      bool
170}
171
172var reportError = func(posn token.Position, message string) {
173	fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message)
174}
175
176// importName renames imports of fromPath within the package specified by info.
177// If fromName is not empty, importName renames only imports as fromName.
178// If the renaming would lead to a conflict, the file is left unchanged.
179func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error {
180	if fromName == to {
181		return nil // no-op (e.g. rename x/foo to y/foo)
182	}
183	for _, f := range info.Files {
184		var from types.Object
185		for _, imp := range f.Imports {
186			importPath, _ := strconv.Unquote(imp.Path.Value)
187			importName := path.Base(importPath)
188			if imp.Name != nil {
189				importName = imp.Name.Name
190			}
191			if importPath == fromPath && (fromName == "" || importName == fromName) {
192				from = info.Implicits[imp]
193				break
194			}
195		}
196		if from == nil {
197			continue
198		}
199		r := renamer{
200			iprog:        iprog,
201			objsToUpdate: make(map[types.Object]bool),
202			to:           to,
203			packages:     map[*types.Package]*loader.PackageInfo{info.Pkg: info},
204		}
205		r.check(from)
206		if r.hadConflicts {
207			reportError(iprog.Fset.Position(f.Imports[0].Pos()),
208				"skipping update of this file")
209			continue // ignore errors; leave the existing name
210		}
211		if err := r.update(); err != nil {
212			return err
213		}
214	}
215	return nil
216}
217
218func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error {
219	// -- Parse the -from or -offset specifier ----------------------------
220
221	if (offsetFlag == "") == (fromFlag == "") {
222		return fmt.Errorf("exactly one of the -from and -offset flags must be specified")
223	}
224
225	if !isValidIdentifier(to) {
226		return fmt.Errorf("-to %q: not a valid identifier", to)
227	}
228
229	if Diff {
230		defer func(saved func(string, []byte) error) { writeFile = saved }(writeFile)
231		writeFile = diff
232	}
233
234	var spec *spec
235	var err error
236	if fromFlag != "" {
237		spec, err = parseFromFlag(ctxt, fromFlag)
238	} else {
239		spec, err = parseOffsetFlag(ctxt, offsetFlag)
240	}
241	if err != nil {
242		return err
243	}
244
245	if spec.fromName == to {
246		return fmt.Errorf("the old and new names are the same: %s", to)
247	}
248
249	// -- Load the program consisting of the initial package  -------------
250
251	iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true})
252	if err != nil {
253		return err
254	}
255
256	fromObjects, err := findFromObjects(iprog, spec)
257	if err != nil {
258		return err
259	}
260
261	// -- Load a larger program, for global renamings ---------------------
262
263	if requiresGlobalRename(fromObjects, to) {
264		// For a local refactoring, we needn't load more
265		// packages, but if the renaming affects the package's
266		// API, we we must load all packages that depend on the
267		// package defining the object, plus their tests.
268
269		if Verbose {
270			log.Print("Potentially global renaming; scanning workspace...")
271		}
272
273		// Scan the workspace and build the import graph.
274		_, rev, errors := importgraph.Build(ctxt)
275		if len(errors) > 0 {
276			// With a large GOPATH tree, errors are inevitable.
277			// Report them but proceed.
278			fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
279			for path, err := range errors {
280				fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
281			}
282		}
283
284		// Enumerate the set of potentially affected packages.
285		affectedPackages := make(map[string]bool)
286		for _, obj := range fromObjects {
287			// External test packages are never imported,
288			// so they will never appear in the graph.
289			for path := range rev.Search(obj.Pkg().Path()) {
290				affectedPackages[path] = true
291			}
292		}
293
294		// TODO(adonovan): allow the user to specify the scope,
295		// or -ignore patterns?  Computing the scope when we
296		// don't (yet) support inputs containing errors can make
297		// the tool rather brittle.
298
299		// Re-load the larger program.
300		iprog, err = loadProgram(ctxt, affectedPackages)
301		if err != nil {
302			return err
303		}
304
305		fromObjects, err = findFromObjects(iprog, spec)
306		if err != nil {
307			return err
308		}
309	}
310
311	// -- Do the renaming -------------------------------------------------
312
313	r := renamer{
314		iprog:        iprog,
315		objsToUpdate: make(map[types.Object]bool),
316		from:         spec.fromName,
317		to:           to,
318		packages:     make(map[*types.Package]*loader.PackageInfo),
319	}
320
321	// A renaming initiated at an interface method indicates the
322	// intention to rename abstract and concrete methods as needed
323	// to preserve assignability.
324	for _, obj := range fromObjects {
325		if obj, ok := obj.(*types.Func); ok {
326			recv := obj.Type().(*types.Signature).Recv()
327			if recv != nil && isInterface(recv.Type().Underlying()) {
328				r.changeMethods = true
329				break
330			}
331		}
332	}
333
334	// Only the initially imported packages (iprog.Imported) and
335	// their external tests (iprog.Created) should be inspected or
336	// modified, as only they have type-checked functions bodies.
337	// The rest are just dependencies, needed only for package-level
338	// type information.
339	for _, info := range iprog.Imported {
340		r.packages[info.Pkg] = info
341	}
342	for _, info := range iprog.Created { // (tests)
343		r.packages[info.Pkg] = info
344	}
345
346	for _, from := range fromObjects {
347		r.check(from)
348	}
349	if r.hadConflicts && !Force {
350		return ConflictError
351	}
352	return r.update()
353}
354
355// loadProgram loads the specified set of packages (plus their tests)
356// and all their dependencies, from source, through the specified build
357// context.  Only packages in pkgs will have their functions bodies typechecked.
358func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) {
359	conf := loader.Config{
360		Build:      ctxt,
361		ParserMode: parser.ParseComments,
362
363		// TODO(adonovan): enable this.  Requires making a lot of code more robust!
364		AllowErrors: false,
365	}
366	// Optimization: don't type-check the bodies of functions in our
367	// dependencies, since we only need exported package members.
368	conf.TypeCheckFuncBodies = func(p string) bool {
369		return pkgs[p] || pkgs[strings.TrimSuffix(p, "_test")]
370	}
371
372	if Verbose {
373		var list []string
374		for pkg := range pkgs {
375			list = append(list, pkg)
376		}
377		sort.Strings(list)
378		for _, pkg := range list {
379			log.Printf("Loading package: %s", pkg)
380		}
381	}
382
383	for pkg := range pkgs {
384		conf.ImportWithTests(pkg)
385	}
386
387	// Ideally we would just return conf.Load() here, but go/types
388	// reports certain "soft" errors that gc does not (Go issue 14596).
389	// As a workaround, we set AllowErrors=true and then duplicate
390	// the loader's error checking but allow soft errors.
391	// It would be nice if the loader API permitted "AllowErrors: soft".
392	conf.AllowErrors = true
393	prog, err := conf.Load()
394	if err != nil {
395		return nil, err
396	}
397
398	var errpkgs []string
399	// Report hard errors in indirectly imported packages.
400	for _, info := range prog.AllPackages {
401		if containsHardErrors(info.Errors) {
402			errpkgs = append(errpkgs, info.Pkg.Path())
403		}
404	}
405	if errpkgs != nil {
406		var more string
407		if len(errpkgs) > 3 {
408			more = fmt.Sprintf(" and %d more", len(errpkgs)-3)
409			errpkgs = errpkgs[:3]
410		}
411		return nil, fmt.Errorf("couldn't load packages due to errors: %s%s",
412			strings.Join(errpkgs, ", "), more)
413	}
414	return prog, nil
415}
416
417func containsHardErrors(errors []error) bool {
418	for _, err := range errors {
419		if err, ok := err.(types.Error); ok && err.Soft {
420			continue
421		}
422		return true
423	}
424	return false
425}
426
427// requiresGlobalRename reports whether this renaming could potentially
428// affect other packages in the Go workspace.
429func requiresGlobalRename(fromObjects []types.Object, to string) bool {
430	var tfm bool
431	for _, from := range fromObjects {
432		if from.Exported() {
433			return true
434		}
435		switch objectKind(from) {
436		case "type", "field", "method":
437			tfm = true
438		}
439	}
440	if ast.IsExported(to) && tfm {
441		// A global renaming may be necessary even if we're
442		// exporting a previous unexported name, since if it's
443		// the name of a type, field or method, this could
444		// change selections in other packages.
445		// (We include "type" in this list because a type
446		// used as an embedded struct field entails a field
447		// renaming.)
448		return true
449	}
450	return false
451}
452
453// update updates the input files.
454func (r *renamer) update() error {
455	// We use token.File, not filename, since a file may appear to
456	// belong to multiple packages and be parsed more than once.
457	// token.File captures this distinction; filename does not.
458
459	var nidents int
460	var filesToUpdate = make(map[*token.File]bool)
461	docRegexp := regexp.MustCompile(`\b` + r.from + `\b`)
462	for _, info := range r.packages {
463		// Mutate the ASTs and note the filenames.
464		for id, obj := range info.Defs {
465			if r.objsToUpdate[obj] {
466				nidents++
467				id.Name = r.to
468				filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
469				// Perform the rename in doc comments too.
470				if doc := r.docComment(id); doc != nil {
471					for _, comment := range doc.List {
472						comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to)
473					}
474				}
475			}
476		}
477
478		for id, obj := range info.Uses {
479			if r.objsToUpdate[obj] {
480				nidents++
481				id.Name = r.to
482				filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
483			}
484		}
485	}
486
487	// Renaming not supported if cgo files are affected.
488	var generatedFileNames []string
489	for _, info := range r.packages {
490		for _, f := range info.Files {
491			tokenFile := r.iprog.Fset.File(f.Pos())
492			if filesToUpdate[tokenFile] && generated(f, tokenFile) {
493				generatedFileNames = append(generatedFileNames, tokenFile.Name())
494			}
495		}
496	}
497	if !Force && len(generatedFileNames) > 0 {
498		return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v", plural(len(generatedFileNames)), generatedFileNames)
499	}
500
501	// Write affected files.
502	var nerrs, npkgs int
503	for _, info := range r.packages {
504		first := true
505		for _, f := range info.Files {
506			tokenFile := r.iprog.Fset.File(f.Pos())
507			if filesToUpdate[tokenFile] {
508				if first {
509					npkgs++
510					first = false
511					if Verbose {
512						log.Printf("Updating package %s", info.Pkg.Path())
513					}
514				}
515
516				filename := tokenFile.Name()
517				var buf bytes.Buffer
518				if err := format.Node(&buf, r.iprog.Fset, f); err != nil {
519					log.Printf("failed to pretty-print syntax tree: %v", err)
520					nerrs++
521					continue
522				}
523				if err := writeFile(filename, buf.Bytes()); err != nil {
524					log.Print(err)
525					nerrs++
526				}
527			}
528		}
529	}
530	if !Diff {
531		fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n",
532			nidents, plural(nidents),
533			len(filesToUpdate), plural(len(filesToUpdate)),
534			npkgs, plural(npkgs))
535	}
536	if nerrs > 0 {
537		return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs))
538	}
539	return nil
540}
541
542// docComment returns the doc for an identifier.
543func (r *renamer) docComment(id *ast.Ident) *ast.CommentGroup {
544	_, nodes, _ := r.iprog.PathEnclosingInterval(id.Pos(), id.End())
545	for _, node := range nodes {
546		switch decl := node.(type) {
547		case *ast.FuncDecl:
548			return decl.Doc
549		case *ast.Field:
550			return decl.Doc
551		case *ast.GenDecl:
552			return decl.Doc
553		// For {Type,Value}Spec, if the doc on the spec is absent,
554		// search for the enclosing GenDecl
555		case *ast.TypeSpec:
556			if decl.Doc != nil {
557				return decl.Doc
558			}
559		case *ast.ValueSpec:
560			if decl.Doc != nil {
561				return decl.Doc
562			}
563		case *ast.Ident:
564		default:
565			return nil
566		}
567	}
568	return nil
569}
570
571func plural(n int) string {
572	if n != 1 {
573		return "s"
574	}
575	return ""
576}
577
578// writeFile is a seam for testing and for the -d flag.
579var writeFile = reallyWriteFile
580
581func reallyWriteFile(filename string, content []byte) error {
582	return ioutil.WriteFile(filename, content, 0644)
583}
584
585func diff(filename string, content []byte) error {
586	renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid())
587	if err := ioutil.WriteFile(renamed, content, 0644); err != nil {
588		return err
589	}
590	defer os.Remove(renamed)
591
592	diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput()
593	if len(diff) > 0 {
594		// diff exits with a non-zero status when the files don't match.
595		// Ignore that failure as long as we get output.
596		stdout.Write(diff)
597		return nil
598	}
599	if err != nil {
600		return fmt.Errorf("computing diff: %v", err)
601	}
602	return nil
603}
604