// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // licence 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" "log" "os" "os/exec" "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) }