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  }