| // 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" | |
| "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.FileStart) | |
| 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) | |
| } |