| // Copyright 2015 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. | 
 |  | 
 | // This file contains the implementation of the 'gomvpkg' command | 
 | // whose main function is in golang.org/x/tools/cmd/gomvpkg. | 
 |  | 
 | package rename | 
 |  | 
 | // TODO(matloob): | 
 | // - think about what happens if the package is moving across version control systems. | 
 | // - dot imports are not supported. Make sure it's clearly documented. | 
 |  | 
 | import ( | 
 | 	"bytes" | 
 | 	"fmt" | 
 | 	"go/ast" | 
 | 	"go/build" | 
 | 	"go/format" | 
 | 	"go/token" | 
 | 	exec "golang.org/x/sys/execabs" | 
 | 	"log" | 
 | 	"os" | 
 | 	"path" | 
 | 	"path/filepath" | 
 | 	"regexp" | 
 | 	"runtime" | 
 | 	"strconv" | 
 | 	"strings" | 
 | 	"text/template" | 
 |  | 
 | 	"golang.org/x/tools/go/buildutil" | 
 | 	"golang.org/x/tools/go/loader" | 
 | 	"golang.org/x/tools/refactor/importgraph" | 
 | ) | 
 |  | 
 | // Move, given a package path and a destination package path, will try | 
 | // to move the given package to the new path. The Move function will | 
 | // first check for any conflicts preventing the move, such as a | 
 | // package already existing at the destination package path. If the | 
 | // move can proceed, it builds an import graph to find all imports of | 
 | // the packages whose paths need to be renamed. This includes uses of | 
 | // the subpackages of the package to be moved as those packages will | 
 | // also need to be moved. It then renames all imports to point to the | 
 | // new paths, and then moves the packages to their new paths. | 
 | func Move(ctxt *build.Context, from, to, moveTmpl string) error { | 
 | 	srcDir, err := srcDir(ctxt, from) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	// This should be the only place in the program that constructs | 
 | 	// file paths. | 
 | 	fromDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(from)) | 
 | 	toDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(to)) | 
 | 	toParent := filepath.Dir(toDir) | 
 | 	if !buildutil.IsDir(ctxt, toParent) { | 
 | 		return fmt.Errorf("parent directory does not exist for path %s", toDir) | 
 | 	} | 
 |  | 
 | 	// Build the import graph and figure out which packages to update. | 
 | 	_, rev, errors := importgraph.Build(ctxt) | 
 | 	if len(errors) > 0 { | 
 | 		// With a large GOPATH tree, errors are inevitable. | 
 | 		// Report them but proceed. | 
 | 		fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n") | 
 | 		for path, err := range errors { | 
 | 			fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Determine the affected packages---the set of packages whose import | 
 | 	// statements need updating. | 
 | 	affectedPackages := map[string]bool{from: true} | 
 | 	destinations := make(map[string]string) // maps old import path to new import path | 
 | 	for pkg := range subpackages(ctxt, srcDir, from) { | 
 | 		for r := range rev[pkg] { | 
 | 			affectedPackages[r] = true | 
 | 		} | 
 | 		destinations[pkg] = strings.Replace(pkg, from, to, 1) | 
 | 	} | 
 |  | 
 | 	// Load all the affected packages. | 
 | 	iprog, err := loadProgram(ctxt, affectedPackages) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	// Prepare the move command, if one was supplied. | 
 | 	var cmd string | 
 | 	if moveTmpl != "" { | 
 | 		if cmd, err = moveCmd(moveTmpl, fromDir, toDir); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	m := mover{ | 
 | 		ctxt:             ctxt, | 
 | 		rev:              rev, | 
 | 		iprog:            iprog, | 
 | 		from:             from, | 
 | 		to:               to, | 
 | 		fromDir:          fromDir, | 
 | 		toDir:            toDir, | 
 | 		affectedPackages: affectedPackages, | 
 | 		destinations:     destinations, | 
 | 		cmd:              cmd, | 
 | 	} | 
 |  | 
 | 	if err := m.checkValid(); err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	m.move() | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | // srcDir returns the absolute path of the srcdir containing pkg. | 
 | func srcDir(ctxt *build.Context, pkg string) (string, error) { | 
 | 	for _, srcDir := range ctxt.SrcDirs() { | 
 | 		path := buildutil.JoinPath(ctxt, srcDir, pkg) | 
 | 		if buildutil.IsDir(ctxt, path) { | 
 | 			return srcDir, nil | 
 | 		} | 
 | 	} | 
 | 	return "", fmt.Errorf("src dir not found for package: %s", pkg) | 
 | } | 
 |  | 
 | // subpackages returns the set of packages in the given srcDir whose | 
 | // import path equals to root, or has "root/" as the prefix. | 
 | func subpackages(ctxt *build.Context, srcDir string, root string) map[string]bool { | 
 | 	var subs = make(map[string]bool) | 
 | 	buildutil.ForEachPackage(ctxt, func(pkg string, err error) { | 
 | 		if err != nil { | 
 | 			log.Fatalf("unexpected error in ForEachPackage: %v", err) | 
 | 		} | 
 |  | 
 | 		// Only process the package root, or a sub-package of it. | 
 | 		if !(strings.HasPrefix(pkg, root) && | 
 | 			(len(pkg) == len(root) || pkg[len(root)] == '/')) { | 
 | 			return | 
 | 		} | 
 |  | 
 | 		p, err := ctxt.Import(pkg, "", build.FindOnly) | 
 | 		if err != nil { | 
 | 			log.Fatalf("unexpected: package %s can not be located by build context: %s", pkg, err) | 
 | 		} | 
 | 		if p.SrcRoot == "" { | 
 | 			log.Fatalf("unexpected: could not determine srcDir for package %s: %s", pkg, err) | 
 | 		} | 
 | 		if p.SrcRoot != srcDir { | 
 | 			return | 
 | 		} | 
 |  | 
 | 		subs[pkg] = true | 
 | 	}) | 
 | 	return subs | 
 | } | 
 |  | 
 | type mover struct { | 
 | 	// iprog contains all packages whose contents need to be updated | 
 | 	// with new package names or import paths. | 
 | 	iprog *loader.Program | 
 | 	ctxt  *build.Context | 
 | 	// rev is the reverse import graph. | 
 | 	rev importgraph.Graph | 
 | 	// from and to are the source and destination import | 
 | 	// paths. fromDir and toDir are the source and destination | 
 | 	// absolute paths that package source files will be moved between. | 
 | 	from, to, fromDir, toDir string | 
 | 	// affectedPackages is the set of all packages whose contents need | 
 | 	// to be updated to reflect new package names or import paths. | 
 | 	affectedPackages map[string]bool | 
 | 	// destinations maps each subpackage to be moved to its | 
 | 	// destination path. | 
 | 	destinations map[string]string | 
 | 	// cmd, if not empty, will be executed to move fromDir to toDir. | 
 | 	cmd string | 
 | } | 
 |  | 
 | func (m *mover) checkValid() error { | 
 | 	const prefix = "invalid move destination" | 
 |  | 
 | 	match, err := regexp.MatchString("^[_\\pL][_\\pL\\p{Nd}]*$", path.Base(m.to)) | 
 | 	if err != nil { | 
 | 		panic("regexp.MatchString failed") | 
 | 	} | 
 | 	if !match { | 
 | 		return fmt.Errorf("%s: %s; gomvpkg does not support move destinations "+ | 
 | 			"whose base names are not valid go identifiers", prefix, m.to) | 
 | 	} | 
 |  | 
 | 	if buildutil.FileExists(m.ctxt, m.toDir) { | 
 | 		return fmt.Errorf("%s: %s conflicts with file %s", prefix, m.to, m.toDir) | 
 | 	} | 
 | 	if buildutil.IsDir(m.ctxt, m.toDir) { | 
 | 		return fmt.Errorf("%s: %s conflicts with directory %s", prefix, m.to, m.toDir) | 
 | 	} | 
 |  | 
 | 	for _, toSubPkg := range m.destinations { | 
 | 		if _, err := m.ctxt.Import(toSubPkg, "", build.FindOnly); err == nil { | 
 | 			return fmt.Errorf("%s: %s; package or subpackage %s already exists", | 
 | 				prefix, m.to, toSubPkg) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | // moveCmd produces the version control move command used to move fromDir to toDir by | 
 | // executing the given template. | 
 | func moveCmd(moveTmpl, fromDir, toDir string) (string, error) { | 
 | 	tmpl, err := template.New("movecmd").Parse(moveTmpl) | 
 | 	if err != nil { | 
 | 		return "", err | 
 | 	} | 
 |  | 
 | 	var buf bytes.Buffer | 
 | 	err = tmpl.Execute(&buf, struct { | 
 | 		Src string | 
 | 		Dst string | 
 | 	}{fromDir, toDir}) | 
 | 	return buf.String(), err | 
 | } | 
 |  | 
 | func (m *mover) move() error { | 
 | 	filesToUpdate := make(map[*ast.File]bool) | 
 |  | 
 | 	// Change the moved package's "package" declaration to its new base name. | 
 | 	pkg, ok := m.iprog.Imported[m.from] | 
 | 	if !ok { | 
 | 		log.Fatalf("unexpected: package %s is not in import map", m.from) | 
 | 	} | 
 | 	newName := filepath.Base(m.to) | 
 | 	for _, f := range pkg.Files { | 
 | 		// Update all import comments. | 
 | 		for _, cg := range f.Comments { | 
 | 			c := cg.List[0] | 
 | 			if c.Slash >= f.Name.End() && | 
 | 				sameLine(m.iprog.Fset, c.Slash, f.Name.End()) && | 
 | 				(f.Decls == nil || c.Slash < f.Decls[0].Pos()) { | 
 | 				if strings.HasPrefix(c.Text, `// import "`) { | 
 | 					c.Text = `// import "` + m.to + `"` | 
 | 					break | 
 | 				} | 
 | 				if strings.HasPrefix(c.Text, `/* import "`) { | 
 | 					c.Text = `/* import "` + m.to + `" */` | 
 | 					break | 
 | 				} | 
 | 			} | 
 | 		} | 
 | 		f.Name.Name = newName // change package decl | 
 | 		filesToUpdate[f] = true | 
 | 	} | 
 |  | 
 | 	// Look through the external test packages (m.iprog.Created contains the external test packages). | 
 | 	for _, info := range m.iprog.Created { | 
 | 		// Change the "package" declaration of the external test package. | 
 | 		if info.Pkg.Path() == m.from+"_test" { | 
 | 			for _, f := range info.Files { | 
 | 				f.Name.Name = newName + "_test" // change package decl | 
 | 				filesToUpdate[f] = true | 
 | 			} | 
 | 		} | 
 |  | 
 | 		// Mark all the loaded external test packages, which import the "from" package, | 
 | 		// as affected packages and update the imports. | 
 | 		for _, imp := range info.Pkg.Imports() { | 
 | 			if imp.Path() == m.from { | 
 | 				m.affectedPackages[info.Pkg.Path()] = true | 
 | 				m.iprog.Imported[info.Pkg.Path()] = info | 
 | 				if err := importName(m.iprog, info, m.from, path.Base(m.from), newName); err != nil { | 
 | 					return err | 
 | 				} | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Update imports of that package to use the new import name. | 
 | 	// None of the subpackages will change their name---only the from package | 
 | 	// itself will. | 
 | 	for p := range m.rev[m.from] { | 
 | 		if err := importName(m.iprog, m.iprog.Imported[p], m.from, path.Base(m.from), newName); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Update import paths for all imports by affected packages. | 
 | 	for ap := range m.affectedPackages { | 
 | 		info, ok := m.iprog.Imported[ap] | 
 | 		if !ok { | 
 | 			log.Fatalf("unexpected: package %s is not in import map", ap) | 
 | 		} | 
 | 		for _, f := range info.Files { | 
 | 			for _, imp := range f.Imports { | 
 | 				importPath, _ := strconv.Unquote(imp.Path.Value) | 
 | 				if newPath, ok := m.destinations[importPath]; ok { | 
 | 					imp.Path.Value = strconv.Quote(newPath) | 
 |  | 
 | 					oldName := path.Base(importPath) | 
 | 					if imp.Name != nil { | 
 | 						oldName = imp.Name.Name | 
 | 					} | 
 |  | 
 | 					newName := path.Base(newPath) | 
 | 					if imp.Name == nil && oldName != newName { | 
 | 						imp.Name = ast.NewIdent(oldName) | 
 | 					} else if imp.Name == nil || imp.Name.Name == newName { | 
 | 						imp.Name = nil | 
 | 					} | 
 | 					filesToUpdate[f] = true | 
 | 				} | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	for f := range filesToUpdate { | 
 | 		var buf bytes.Buffer | 
 | 		if err := format.Node(&buf, m.iprog.Fset, f); err != nil { | 
 | 			log.Printf("failed to pretty-print syntax tree: %v", err) | 
 | 			continue | 
 | 		} | 
 | 		tokenFile := m.iprog.Fset.File(f.Pos()) | 
 | 		writeFile(tokenFile.Name(), buf.Bytes()) | 
 | 	} | 
 |  | 
 | 	// Move the directories. | 
 | 	// If either the fromDir or toDir are contained under version control it is | 
 | 	// the user's responsibility to provide a custom move command that updates | 
 | 	// version control to reflect the move. | 
 | 	// TODO(matloob): If the parent directory of toDir does not exist, create it. | 
 | 	//      For now, it's required that it does exist. | 
 |  | 
 | 	if m.cmd != "" { | 
 | 		// TODO(matloob): Verify that the windows and plan9 cases are correct. | 
 | 		var cmd *exec.Cmd | 
 | 		switch runtime.GOOS { | 
 | 		case "windows": | 
 | 			cmd = exec.Command("cmd", "/c", m.cmd) | 
 | 		case "plan9": | 
 | 			cmd = exec.Command("rc", "-c", m.cmd) | 
 | 		default: | 
 | 			cmd = exec.Command("sh", "-c", m.cmd) | 
 | 		} | 
 | 		cmd.Stderr = os.Stderr | 
 | 		cmd.Stdout = os.Stdout | 
 | 		if err := cmd.Run(); err != nil { | 
 | 			return fmt.Errorf("version control system's move command failed: %v", err) | 
 | 		} | 
 |  | 
 | 		return nil | 
 | 	} | 
 |  | 
 | 	return moveDirectory(m.fromDir, m.toDir) | 
 | } | 
 |  | 
 | // sameLine reports whether two positions in the same file are on the same line. | 
 | func sameLine(fset *token.FileSet, x, y token.Pos) bool { | 
 | 	return fset.Position(x).Line == fset.Position(y).Line | 
 | } | 
 |  | 
 | var moveDirectory = func(from, to string) error { | 
 | 	return os.Rename(from, to) | 
 | } |