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