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