1// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// licence that can be found in the LICENSE file.
4
5// This file contains the implementation of the 'gomvpkg' command
6// whose main function is in golang.org/x/tools/cmd/gomvpkg.
7
8package rename
9
10// TODO(matloob):
11// - think about what happens if the package is moving across version control systems.
12// - dot imports are not supported. Make sure it's clearly documented.
13
14import (
15	"bytes"
16	"fmt"
17	"go/ast"
18	"go/build"
19	"go/format"
20	"go/token"
21	"log"
22	"os"
23	"os/exec"
24	"path"
25	"path/filepath"
26	"regexp"
27	"runtime"
28	"strconv"
29	"strings"
30	"text/template"
31
32	"golang.org/x/tools/go/buildutil"
33	"golang.org/x/tools/go/loader"
34	"golang.org/x/tools/refactor/importgraph"
35)
36
37// Move, given a package path and a destination package path, will try
38// to move the given package to the new path. The Move function will
39// first check for any conflicts preventing the move, such as a
40// package already existing at the destination package path. If the
41// move can proceed, it builds an import graph to find all imports of
42// the packages whose paths need to be renamed. This includes uses of
43// the subpackages of the package to be moved as those packages will
44// also need to be moved. It then renames all imports to point to the
45// new paths, and then moves the packages to their new paths.
46func Move(ctxt *build.Context, from, to, moveTmpl string) error {
47	srcDir, err := srcDir(ctxt, from)
48	if err != nil {
49		return err
50	}
51
52	// This should be the only place in the program that constructs
53	// file paths.
54	fromDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(from))
55	toDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(to))
56	toParent := filepath.Dir(toDir)
57	if !buildutil.IsDir(ctxt, toParent) {
58		return fmt.Errorf("parent directory does not exist for path %s", toDir)
59	}
60
61	// Build the import graph and figure out which packages to update.
62	_, rev, errors := importgraph.Build(ctxt)
63	if len(errors) > 0 {
64		// With a large GOPATH tree, errors are inevitable.
65		// Report them but proceed.
66		fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
67		for path, err := range errors {
68			fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
69		}
70	}
71
72	// Determine the affected packages---the set of packages whose import
73	// statements need updating.
74	affectedPackages := map[string]bool{from: true}
75	destinations := make(map[string]string) // maps old import path to new import path
76	for pkg := range subpackages(ctxt, srcDir, from) {
77		for r := range rev[pkg] {
78			affectedPackages[r] = true
79		}
80		destinations[pkg] = strings.Replace(pkg, from, to, 1)
81	}
82
83	// Load all the affected packages.
84	iprog, err := loadProgram(ctxt, affectedPackages)
85	if err != nil {
86		return err
87	}
88
89	// Prepare the move command, if one was supplied.
90	var cmd string
91	if moveTmpl != "" {
92		if cmd, err = moveCmd(moveTmpl, fromDir, toDir); err != nil {
93			return err
94		}
95	}
96
97	m := mover{
98		ctxt:             ctxt,
99		rev:              rev,
100		iprog:            iprog,
101		from:             from,
102		to:               to,
103		fromDir:          fromDir,
104		toDir:            toDir,
105		affectedPackages: affectedPackages,
106		destinations:     destinations,
107		cmd:              cmd,
108	}
109
110	if err := m.checkValid(); err != nil {
111		return err
112	}
113
114	m.move()
115
116	return nil
117}
118
119// srcDir returns the absolute path of the srcdir containing pkg.
120func srcDir(ctxt *build.Context, pkg string) (string, error) {
121	for _, srcDir := range ctxt.SrcDirs() {
122		path := buildutil.JoinPath(ctxt, srcDir, pkg)
123		if buildutil.IsDir(ctxt, path) {
124			return srcDir, nil
125		}
126	}
127	return "", fmt.Errorf("src dir not found for package: %s", pkg)
128}
129
130// subpackages returns the set of packages in the given srcDir whose
131// import path equals to root, or has "root/" as the prefix.
132func subpackages(ctxt *build.Context, srcDir string, root string) map[string]bool {
133	var subs = make(map[string]bool)
134	buildutil.ForEachPackage(ctxt, func(pkg string, err error) {
135		if err != nil {
136			log.Fatalf("unexpected error in ForEachPackage: %v", err)
137		}
138
139		// Only process the package root, or a sub-package of it.
140		if !(strings.HasPrefix(pkg, root) &&
141			(len(pkg) == len(root) || pkg[len(root)] == '/')) {
142			return
143		}
144
145		p, err := ctxt.Import(pkg, "", build.FindOnly)
146		if err != nil {
147			log.Fatalf("unexpected: package %s can not be located by build context: %s", pkg, err)
148		}
149		if p.SrcRoot == "" {
150			log.Fatalf("unexpected: could not determine srcDir for package %s: %s", pkg, err)
151		}
152		if p.SrcRoot != srcDir {
153			return
154		}
155
156		subs[pkg] = true
157	})
158	return subs
159}
160
161type mover struct {
162	// iprog contains all packages whose contents need to be updated
163	// with new package names or import paths.
164	iprog *loader.Program
165	ctxt  *build.Context
166	// rev is the reverse import graph.
167	rev importgraph.Graph
168	// from and to are the source and destination import
169	// paths. fromDir and toDir are the source and destination
170	// absolute paths that package source files will be moved between.
171	from, to, fromDir, toDir string
172	// affectedPackages is the set of all packages whose contents need
173	// to be updated to reflect new package names or import paths.
174	affectedPackages map[string]bool
175	// destinations maps each subpackage to be moved to its
176	// destination path.
177	destinations map[string]string
178	// cmd, if not empty, will be executed to move fromDir to toDir.
179	cmd string
180}
181
182func (m *mover) checkValid() error {
183	const prefix = "invalid move destination"
184
185	match, err := regexp.MatchString("^[_\\pL][_\\pL\\p{Nd}]*$", path.Base(m.to))
186	if err != nil {
187		panic("regexp.MatchString failed")
188	}
189	if !match {
190		return fmt.Errorf("%s: %s; gomvpkg does not support move destinations "+
191			"whose base names are not valid go identifiers", prefix, m.to)
192	}
193
194	if buildutil.FileExists(m.ctxt, m.toDir) {
195		return fmt.Errorf("%s: %s conflicts with file %s", prefix, m.to, m.toDir)
196	}
197	if buildutil.IsDir(m.ctxt, m.toDir) {
198		return fmt.Errorf("%s: %s conflicts with directory %s", prefix, m.to, m.toDir)
199	}
200
201	for _, toSubPkg := range m.destinations {
202		if _, err := m.ctxt.Import(toSubPkg, "", build.FindOnly); err == nil {
203			return fmt.Errorf("%s: %s; package or subpackage %s already exists",
204				prefix, m.to, toSubPkg)
205		}
206	}
207
208	return nil
209}
210
211// moveCmd produces the version control move command used to move fromDir to toDir by
212// executing the given template.
213func moveCmd(moveTmpl, fromDir, toDir string) (string, error) {
214	tmpl, err := template.New("movecmd").Parse(moveTmpl)
215	if err != nil {
216		return "", err
217	}
218
219	var buf bytes.Buffer
220	err = tmpl.Execute(&buf, struct {
221		Src string
222		Dst string
223	}{fromDir, toDir})
224	return buf.String(), err
225}
226
227func (m *mover) move() error {
228	filesToUpdate := make(map[*ast.File]bool)
229
230	// Change the moved package's "package" declaration to its new base name.
231	pkg, ok := m.iprog.Imported[m.from]
232	if !ok {
233		log.Fatalf("unexpected: package %s is not in import map", m.from)
234	}
235	newName := filepath.Base(m.to)
236	for _, f := range pkg.Files {
237		// Update all import comments.
238		for _, cg := range f.Comments {
239			c := cg.List[0]
240			if c.Slash >= f.Name.End() &&
241				sameLine(m.iprog.Fset, c.Slash, f.Name.End()) &&
242				(f.Decls == nil || c.Slash < f.Decls[0].Pos()) {
243				if strings.HasPrefix(c.Text, `// import "`) {
244					c.Text = `// import "` + m.to + `"`
245					break
246				}
247				if strings.HasPrefix(c.Text, `/* import "`) {
248					c.Text = `/* import "` + m.to + `" */`
249					break
250				}
251			}
252		}
253		f.Name.Name = newName // change package decl
254		filesToUpdate[f] = true
255	}
256
257	// Look through the external test packages (m.iprog.Created contains the external test packages).
258	for _, info := range m.iprog.Created {
259		// Change the "package" declaration of the external test package.
260		if info.Pkg.Path() == m.from+"_test" {
261			for _, f := range info.Files {
262				f.Name.Name = newName + "_test" // change package decl
263				filesToUpdate[f] = true
264			}
265		}
266
267		// Mark all the loaded external test packages, which import the "from" package,
268		// as affected packages and update the imports.
269		for _, imp := range info.Pkg.Imports() {
270			if imp.Path() == m.from {
271				m.affectedPackages[info.Pkg.Path()] = true
272				m.iprog.Imported[info.Pkg.Path()] = info
273				if err := importName(m.iprog, info, m.from, path.Base(m.from), newName); err != nil {
274					return err
275				}
276			}
277		}
278	}
279
280	// Update imports of that package to use the new import name.
281	// None of the subpackages will change their name---only the from package
282	// itself will.
283	for p := range m.rev[m.from] {
284		if err := importName(m.iprog, m.iprog.Imported[p], m.from, path.Base(m.from), newName); err != nil {
285			return err
286		}
287	}
288
289	// Update import paths for all imports by affected packages.
290	for ap := range m.affectedPackages {
291		info, ok := m.iprog.Imported[ap]
292		if !ok {
293			log.Fatalf("unexpected: package %s is not in import map", ap)
294		}
295		for _, f := range info.Files {
296			for _, imp := range f.Imports {
297				importPath, _ := strconv.Unquote(imp.Path.Value)
298				if newPath, ok := m.destinations[importPath]; ok {
299					imp.Path.Value = strconv.Quote(newPath)
300
301					oldName := path.Base(importPath)
302					if imp.Name != nil {
303						oldName = imp.Name.Name
304					}
305
306					newName := path.Base(newPath)
307					if imp.Name == nil && oldName != newName {
308						imp.Name = ast.NewIdent(oldName)
309					} else if imp.Name == nil || imp.Name.Name == newName {
310						imp.Name = nil
311					}
312					filesToUpdate[f] = true
313				}
314			}
315		}
316	}
317
318	for f := range filesToUpdate {
319		var buf bytes.Buffer
320		if err := format.Node(&buf, m.iprog.Fset, f); err != nil {
321			log.Printf("failed to pretty-print syntax tree: %v", err)
322			continue
323		}
324		tokenFile := m.iprog.Fset.File(f.Pos())
325		writeFile(tokenFile.Name(), buf.Bytes())
326	}
327
328	// Move the directories.
329	// If either the fromDir or toDir are contained under version control it is
330	// the user's responsibility to provide a custom move command that updates
331	// version control to reflect the move.
332	// TODO(matloob): If the parent directory of toDir does not exist, create it.
333	//      For now, it's required that it does exist.
334
335	if m.cmd != "" {
336		// TODO(matloob): Verify that the windows and plan9 cases are correct.
337		var cmd *exec.Cmd
338		switch runtime.GOOS {
339		case "windows":
340			cmd = exec.Command("cmd", "/c", m.cmd)
341		case "plan9":
342			cmd = exec.Command("rc", "-c", m.cmd)
343		default:
344			cmd = exec.Command("sh", "-c", m.cmd)
345		}
346		cmd.Stderr = os.Stderr
347		cmd.Stdout = os.Stdout
348		if err := cmd.Run(); err != nil {
349			return fmt.Errorf("version control system's move command failed: %v", err)
350		}
351
352		return nil
353	}
354
355	return moveDirectory(m.fromDir, m.toDir)
356}
357
358// sameLine reports whether two positions in the same file are on the same line.
359func sameLine(fset *token.FileSet, x, y token.Pos) bool {
360	return fset.Position(x).Line == fset.Position(y).Line
361}
362
363var moveDirectory = func(from, to string) error {
364	return os.Rename(from, to)
365}
366