GoPLS Viewer

Home|gopls/refactor/rename/mvpkg.go
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
8package 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
14import (
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.
46func Move(ctxt *build.ContextfromtomoveTmpl stringerror {
47    srcDirerr := srcDir(ctxtfrom)
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(ctxtsrcDirfilepath.FromSlash(from))
55    toDir := buildutil.JoinPath(ctxtsrcDirfilepath.FromSlash(to))
56    toParent := filepath.Dir(toDir)
57    if !buildutil.IsDir(ctxttoParent) {
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    _reverrors := 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 patherr := range errors {
68            fmt.Fprintf(os.Stderr"Package %q: %s.\n"patherr)
69        }
70    }
71
72    // Determine the affected packages---the set of packages whose import
73    // statements need updating.
74    affectedPackages := map[string]bool{fromtrue}
75    destinations := make(map[string]string// maps old import path to new import path
76    for pkg := range subpackages(ctxtsrcDirfrom) {
77        for r := range rev[pkg] {
78            affectedPackages[r] = true
79        }
80        destinations[pkg] = strings.Replace(pkgfromto1)
81    }
82
83    // Load all the affected packages.
84    iprogerr := loadProgram(ctxtaffectedPackages)
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 cmderr = moveCmd(moveTmplfromDirtoDir); 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        affectedPackagesaffectedPackages,
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.
120func srcDir(ctxt *build.Contextpkg string) (stringerror) {
121    for _srcDir := range ctxt.SrcDirs() {
122        path := buildutil.JoinPath(ctxtsrcDirpkg)
123        if buildutil.IsDir(ctxtpath) {
124            return srcDirnil
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.
132func subpackages(ctxt *build.ContextsrcDir stringroot string) map[string]bool {
133    var subs = make(map[string]bool)
134    buildutil.ForEachPackage(ctxt, func(pkg stringerr 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(pkgroot) &&
141            (len(pkg) == len(root) || pkg[len(root)] == '/')) {
142            return
143        }
144
145        perr := ctxt.Import(pkg""build.FindOnly)
146        if err != nil {
147            log.Fatalf("unexpected: package %s can not be located by build context: %s"pkgerr)
148        }
149        if p.SrcRoot == "" {
150            log.Fatalf("unexpected: could not determine srcDir for package %s: %s"pkgerr)
151        }
152        if p.SrcRoot != srcDir {
153            return
154        }
155
156        subs[pkg] = true
157    })
158    return subs
159}
160
161type 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    fromtofromDirtoDir 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
182func (m *movercheckValid() error {
183    const prefix = "invalid move destination"
184
185    matcherr := 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"prefixm.to)
192    }
193
194    if buildutil.FileExists(m.ctxtm.toDir) {
195        return fmt.Errorf("%s: %s conflicts with file %s"prefixm.tom.toDir)
196    }
197    if buildutil.IsDir(m.ctxtm.toDir) {
198        return fmt.Errorf("%s: %s conflicts with directory %s"prefixm.tom.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                prefixm.totoSubPkg)
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.
213func moveCmd(moveTmplfromDirtoDir string) (stringerror) {
214    tmplerr := 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    }{fromDirtoDir})
224    return buf.String(), err
225}
226
227func (m *movermove() error {
228    filesToUpdate := make(map[*ast.File]bool)
229
230    // Change the moved package's "package" declaration to its new base name.
231    pkgok := 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.Fsetc.Slashf.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.iproginfom.frompath.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.iprogm.iprog.Imported[p], m.frompath.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        infook := 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 newPathok := 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(&bufm.iprog.Fsetf); 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.fromDirm.toDir)
356}
357
358// sameLine reports whether two positions in the same file are on the same line.
359func sameLine(fset *token.FileSetxy token.Posbool {
360    return fset.Position(x).Line == fset.Position(y).Line
361}
362
363var moveDirectory = func(fromto stringerror {
364    return os.Rename(fromto)
365}
366
MembersX
Move.srcDir
subpackages.srcDir
mover.to
mover.move.RangeStmt_7680.BlockStmt.BlockStmt.RangeStmt_7831.f
mover.move.RangeStmt_7680.BlockStmt.RangeStmt_8093.imp
mover.move.RangeStmt_8755.BlockStmt.RangeStmt_8909.BlockStmt.RangeStmt_8943.BlockStmt._
log
strings
sameLine.fset
mover.move.RangeStmt_9491.BlockStmt.tokenFile
mover.move.BlockStmt.cmd
srcDir.RangeStmt_3506.srcDir
moveCmd.moveTmpl
moveCmd.buf
mover.move.RangeStmt_9491.BlockStmt.buf
Move.to
Move.errors
mover.checkValid.RangeStmt_6074.toSubPkg
mover.move.m
mover.from
mover.toDir
moveCmd.fromDir
mover.move.RangeStmt_8755.BlockStmt.RangeStmt_8909.BlockStmt.RangeStmt_8943.imp
srcDir
subpackages.BlockStmt.err
importgraph
Move.cmd
subpackages.ctxt
mover.ctxt
mover.cmd
regexp
runtime
mover.move.BlockStmt.err
sameLine
sameLine.x
os
Move.RangeStmt_2527.BlockStmt.RangeStmt_2580.r
Move.BlockStmt.RangeStmt_2194.err
srcDir.ctxt
subpackages.BlockStmt.p
mover
mover.move.RangeStmt_7020.f
mover.move.RangeStmt_7680.BlockStmt.RangeStmt_8093.BlockStmt.BlockStmt.err
build
Move.err
mover.move.RangeStmt_9491.f
mover.move.RangeStmt_8536.BlockStmt.err
mover.move.RangeStmt_8755.BlockStmt.RangeStmt_8909.BlockStmt.RangeStmt_8943.BlockStmt.importPath
Move.toDir
Move.toParent
Move.iprog
srcDir.pkg
subpackages
mover.destinations
path
Move
sameLine.y
mover.checkValid.RangeStmt_6074.BlockStmt._
mover.move.RangeStmt_8755.ap
mover.fromDir
mover.checkValid.m
mover.checkValid.err
moveCmd.err
mover.move.RangeStmt_8755.BlockStmt.RangeStmt_8909.f
buildutil
Move.BlockStmt.RangeStmt_2194.path
Move.destinations
mover.checkValid
exec
template
filepath
srcDir.RangeStmt_3506.BlockStmt.path
Move.rev
mover.rev
mover.move.RangeStmt_7020.BlockStmt.RangeStmt_7085.cg
mover.move.RangeStmt_9491.BlockStmt.err
Move.ctxt
Move.fromDir
mover.move.RangeStmt_7680.info
mover.move.RangeStmt_8536.p
Move._
Move.affectedPackages
moveCmd
moveCmd.toDir
mover.move
mover.move.RangeStmt_8755.BlockStmt.RangeStmt_8909.BlockStmt.RangeStmt_8943.BlockStmt.BlockStmt.newName
mover.iprog
mover.checkValid.RangeStmt_6074.BlockStmt.err
Move.m
mover.affectedPackages
mover.checkValid.prefix
moveCmd.tmpl
mover.move.filesToUpdate
mover.move.newName
format
strconv
Move.moveTmpl
Move.RangeStmt_2527.pkg
subpackages.root
mover.checkValid.match
mover.move.RangeStmt_8755.BlockStmt.RangeStmt_8909.BlockStmt.RangeStmt_8943.BlockStmt.BlockStmt.oldName
bytes
Move.from
Members
X