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