...

Source file src/golang.org/x/tools/cmd/fiximports/main.go

Documentation: golang.org/x/tools/cmd/fiximports

     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  // The fiximports command fixes import declarations to use the canonical
     6  // import path for packages that have an "import comment" as defined by
     7  // https://golang.org/s/go14customimport.
     8  //
     9  // # Background
    10  //
    11  // The Go 1 custom import path mechanism lets the maintainer of a
    12  // package give it a stable name by which clients may import and "go
    13  // get" it, independent of the underlying version control system (such
    14  // as Git) or server (such as github.com) that hosts it.  Requests for
    15  // the custom name are redirected to the underlying name.  This allows
    16  // packages to be migrated from one underlying server or system to
    17  // another without breaking existing clients.
    18  //
    19  // Because this redirect mechanism creates aliases for existing
    20  // packages, it's possible for a single program to import the same
    21  // package by its canonical name and by an alias.  The resulting
    22  // executable will contain two copies of the package, which is wasteful
    23  // at best and incorrect at worst.
    24  //
    25  // To avoid this, "go build" reports an error if it encounters a special
    26  // comment like the one below, and if the import path in the comment
    27  // does not match the path of the enclosing package relative to
    28  // GOPATH/src:
    29  //
    30  //	     $ grep ^package $GOPATH/src/github.com/bob/vanity/foo/foo.go
    31  //		package foo // import "vanity.com/foo"
    32  //
    33  // The error from "go build" indicates that the package canonically
    34  // known as "vanity.com/foo" is locally installed under the
    35  // non-canonical name "github.com/bob/vanity/foo".
    36  //
    37  // # Usage
    38  //
    39  // When a package that you depend on introduces a custom import comment,
    40  // and your workspace imports it by the non-canonical name, your build
    41  // will stop working as soon as you update your copy of that package
    42  // using "go get -u".
    43  //
    44  // The purpose of the fiximports tool is to fix up all imports of the
    45  // non-canonical path within a Go workspace, replacing them with imports
    46  // of the canonical path.  Following a run of fiximports, the workspace
    47  // will no longer depend on the non-canonical copy of the package, so it
    48  // should be safe to delete.  It may be necessary to run "go get -u"
    49  // again to ensure that the package is locally installed under its
    50  // canonical path, if it was not already.
    51  //
    52  // The fiximports tool operates locally; it does not make HTTP requests
    53  // and does not discover new custom import comments.  It only operates
    54  // on non-canonical packages present in your workspace.
    55  //
    56  // The -baddomains flag is a list of domain names that should always be
    57  // considered non-canonical.  You can use this if you wish to make sure
    58  // that you no longer have any dependencies on packages from that
    59  // domain, even those that do not yet provide a canonical import path
    60  // comment.  For example, the default value of -baddomains includes the
    61  // moribund code hosting site code.google.com, so fiximports will report
    62  // an error for each import of a package from this domain remaining
    63  // after canonicalization.
    64  //
    65  // To see the changes fiximports would make without applying them, use
    66  // the -n flag.
    67  package main
    68  
    69  import (
    70  	"bytes"
    71  	"encoding/json"
    72  	"flag"
    73  	"fmt"
    74  	"go/ast"
    75  	"go/format"
    76  	"go/parser"
    77  	"go/token"
    78  	"io"
    79  	"log"
    80  	"os"
    81  	"os/exec"
    82  	"path"
    83  	"path/filepath"
    84  	"sort"
    85  	"strconv"
    86  	"strings"
    87  )
    88  
    89  // flags
    90  var (
    91  	dryrun     = flag.Bool("n", false, "dry run: show changes, but don't apply them")
    92  	badDomains = flag.String("baddomains", "code.google.com",
    93  		"a comma-separated list of domains from which packages should not be imported")
    94  	replaceFlag = flag.String("replace", "",
    95  		"a comma-separated list of noncanonical=canonical pairs of package paths.  If both items in a pair end with '...', they are treated as path prefixes.")
    96  )
    97  
    98  // seams for testing
    99  var (
   100  	stderr    io.Writer = os.Stderr
   101  	writeFile           = os.WriteFile
   102  )
   103  
   104  const usage = `fiximports: rewrite import paths to use canonical package names.
   105  
   106  Usage: fiximports [-n] package...
   107  
   108  The package... arguments specify a list of packages
   109  in the style of the go tool; see "go help packages".
   110  Hint: use "all" or "..." to match the entire workspace.
   111  
   112  For details, see https://pkg.go.dev/golang.org/x/tools/cmd/fiximports
   113  
   114  Flags:
   115    -n:	       dry run: show changes, but don't apply them
   116    -baddomains  a comma-separated list of domains from which packages
   117                 should not be imported
   118  `
   119  
   120  func main() {
   121  	flag.Parse()
   122  
   123  	if len(flag.Args()) == 0 {
   124  		fmt.Fprint(stderr, usage)
   125  		os.Exit(1)
   126  	}
   127  	if !fiximports(flag.Args()...) {
   128  		os.Exit(1)
   129  	}
   130  }
   131  
   132  type canonicalName struct{ path, name string }
   133  
   134  // fiximports fixes imports in the specified packages.
   135  // Invariant: a false result implies an error was already printed.
   136  func fiximports(packages ...string) bool {
   137  	// importedBy is the transpose of the package import graph.
   138  	importedBy := make(map[string]map[*listPackage]bool)
   139  
   140  	// addEdge adds an edge to the import graph.
   141  	addEdge := func(from *listPackage, to string) {
   142  		if to == "C" || to == "unsafe" {
   143  			return // fake
   144  		}
   145  		pkgs := importedBy[to]
   146  		if pkgs == nil {
   147  			pkgs = make(map[*listPackage]bool)
   148  			importedBy[to] = pkgs
   149  		}
   150  		pkgs[from] = true
   151  	}
   152  
   153  	// List metadata for all packages in the workspace.
   154  	pkgs, err := list("...")
   155  	if err != nil {
   156  		fmt.Fprintf(stderr, "importfix: %v\n", err)
   157  		return false
   158  	}
   159  
   160  	// packageName maps each package's path to its name.
   161  	packageName := make(map[string]string)
   162  	for _, p := range pkgs {
   163  		packageName[p.ImportPath] = p.Name
   164  	}
   165  
   166  	// canonical maps each non-canonical package path to
   167  	// its canonical path and name.
   168  	// A present nil value indicates that the canonical package
   169  	// is unknown: hosted on a bad domain with no redirect.
   170  	canonical := make(map[string]canonicalName)
   171  	domains := strings.Split(*badDomains, ",")
   172  
   173  	type replaceItem struct {
   174  		old, new    string
   175  		matchPrefix bool
   176  	}
   177  	var replace []replaceItem
   178  	for _, pair := range strings.Split(*replaceFlag, ",") {
   179  		if pair == "" {
   180  			continue
   181  		}
   182  		words := strings.Split(pair, "=")
   183  		if len(words) != 2 {
   184  			fmt.Fprintf(stderr, "importfix: -replace: %q is not of the form \"canonical=noncanonical\".\n", pair)
   185  			return false
   186  		}
   187  		replace = append(replace, replaceItem{
   188  			old: strings.TrimSuffix(words[0], "..."),
   189  			new: strings.TrimSuffix(words[1], "..."),
   190  			matchPrefix: strings.HasSuffix(words[0], "...") &&
   191  				strings.HasSuffix(words[1], "..."),
   192  		})
   193  	}
   194  
   195  	// Find non-canonical packages and populate importedBy graph.
   196  	for _, p := range pkgs {
   197  		if p.Error != nil {
   198  			msg := p.Error.Err
   199  			if strings.Contains(msg, "code in directory") &&
   200  				strings.Contains(msg, "expects import") {
   201  				// don't show the very errors we're trying to fix
   202  			} else {
   203  				fmt.Fprintln(stderr, p.Error)
   204  			}
   205  		}
   206  
   207  		for _, imp := range p.Imports {
   208  			addEdge(p, imp)
   209  		}
   210  		for _, imp := range p.TestImports {
   211  			addEdge(p, imp)
   212  		}
   213  		for _, imp := range p.XTestImports {
   214  			addEdge(p, imp)
   215  		}
   216  
   217  		// Does package have an explicit import comment?
   218  		if p.ImportComment != "" {
   219  			if p.ImportComment != p.ImportPath {
   220  				canonical[p.ImportPath] = canonicalName{
   221  					path: p.ImportComment,
   222  					name: p.Name,
   223  				}
   224  			}
   225  		} else {
   226  			// Is package matched by a -replace item?
   227  			var newPath string
   228  			for _, item := range replace {
   229  				if item.matchPrefix {
   230  					if strings.HasPrefix(p.ImportPath, item.old) {
   231  						newPath = item.new + p.ImportPath[len(item.old):]
   232  						break
   233  					}
   234  				} else if p.ImportPath == item.old {
   235  					newPath = item.new
   236  					break
   237  				}
   238  			}
   239  			if newPath != "" {
   240  				newName := packageName[newPath]
   241  				if newName == "" {
   242  					newName = filepath.Base(newPath) // a guess
   243  				}
   244  				canonical[p.ImportPath] = canonicalName{
   245  					path: newPath,
   246  					name: newName,
   247  				}
   248  				continue
   249  			}
   250  
   251  			// Is package matched by a -baddomains item?
   252  			for _, domain := range domains {
   253  				slash := strings.Index(p.ImportPath, "/")
   254  				if slash < 0 {
   255  					continue // no slash: standard package
   256  				}
   257  				if p.ImportPath[:slash] == domain {
   258  					// Package comes from bad domain and has no import comment.
   259  					// Report an error each time this package is imported.
   260  					canonical[p.ImportPath] = canonicalName{}
   261  
   262  					// TODO(adonovan): should we make an HTTP request to
   263  					// see if there's an HTTP redirect, a "go-import" meta tag,
   264  					// or an import comment in the latest revision?
   265  					// It would duplicate a lot of logic from "go get".
   266  				}
   267  				break
   268  			}
   269  		}
   270  	}
   271  
   272  	// Find all clients (direct importers) of canonical packages.
   273  	// These are the packages that need fixing up.
   274  	clients := make(map[*listPackage]bool)
   275  	for path := range canonical {
   276  		for client := range importedBy[path] {
   277  			clients[client] = true
   278  		}
   279  	}
   280  
   281  	// Restrict rewrites to the set of packages specified by the user.
   282  	if len(packages) == 1 && (packages[0] == "all" || packages[0] == "...") {
   283  		// no restriction
   284  	} else {
   285  		pkgs, err := list(packages...)
   286  		if err != nil {
   287  			fmt.Fprintf(stderr, "importfix: %v\n", err)
   288  			return false
   289  		}
   290  		seen := make(map[string]bool)
   291  		for _, p := range pkgs {
   292  			seen[p.ImportPath] = true
   293  		}
   294  		for client := range clients {
   295  			if !seen[client.ImportPath] {
   296  				delete(clients, client)
   297  			}
   298  		}
   299  	}
   300  
   301  	// Rewrite selected client packages.
   302  	ok := true
   303  	for client := range clients {
   304  		if !rewritePackage(client, canonical) {
   305  			ok = false
   306  
   307  			// There were errors.
   308  			// Show direct and indirect imports of client.
   309  			seen := make(map[string]bool)
   310  			var direct, indirect []string
   311  			for p := range importedBy[client.ImportPath] {
   312  				direct = append(direct, p.ImportPath)
   313  				seen[p.ImportPath] = true
   314  			}
   315  
   316  			var visit func(path string)
   317  			visit = func(path string) {
   318  				for q := range importedBy[path] {
   319  					qpath := q.ImportPath
   320  					if !seen[qpath] {
   321  						seen[qpath] = true
   322  						indirect = append(indirect, qpath)
   323  						visit(qpath)
   324  					}
   325  				}
   326  			}
   327  
   328  			if direct != nil {
   329  				fmt.Fprintf(stderr, "\timported directly by:\n")
   330  				sort.Strings(direct)
   331  				for _, path := range direct {
   332  					fmt.Fprintf(stderr, "\t\t%s\n", path)
   333  					visit(path)
   334  				}
   335  
   336  				if indirect != nil {
   337  					fmt.Fprintf(stderr, "\timported indirectly by:\n")
   338  					sort.Strings(indirect)
   339  					for _, path := range indirect {
   340  						fmt.Fprintf(stderr, "\t\t%s\n", path)
   341  					}
   342  				}
   343  			}
   344  		}
   345  	}
   346  
   347  	return ok
   348  }
   349  
   350  // Invariant: false result => error already printed.
   351  func rewritePackage(client *listPackage, canonical map[string]canonicalName) bool {
   352  	ok := true
   353  
   354  	used := make(map[string]bool)
   355  	var filenames []string
   356  	filenames = append(filenames, client.GoFiles...)
   357  	filenames = append(filenames, client.TestGoFiles...)
   358  	filenames = append(filenames, client.XTestGoFiles...)
   359  	var first bool
   360  	for _, filename := range filenames {
   361  		if !first {
   362  			first = true
   363  			fmt.Fprintf(stderr, "%s\n", client.ImportPath)
   364  		}
   365  		err := rewriteFile(filepath.Join(client.Dir, filename), canonical, used)
   366  		if err != nil {
   367  			fmt.Fprintf(stderr, "\tERROR: %v\n", err)
   368  			ok = false
   369  		}
   370  	}
   371  
   372  	// Show which imports were renamed in this package.
   373  	var keys []string
   374  	for key := range used {
   375  		keys = append(keys, key)
   376  	}
   377  	sort.Strings(keys)
   378  	for _, key := range keys {
   379  		if p := canonical[key]; p.path != "" {
   380  			fmt.Fprintf(stderr, "\tfixed: %s -> %s\n", key, p.path)
   381  		} else {
   382  			fmt.Fprintf(stderr, "\tERROR: %s has no import comment\n", key)
   383  			ok = false
   384  		}
   385  	}
   386  
   387  	return ok
   388  }
   389  
   390  // rewriteFile reads, modifies, and writes filename, replacing all imports
   391  // of packages P in canonical by canonical[P].
   392  // It records in used which canonical packages were imported.
   393  // used[P]=="" indicates that P was imported but its canonical path is unknown.
   394  func rewriteFile(filename string, canonical map[string]canonicalName, used map[string]bool) error {
   395  	fset := token.NewFileSet()
   396  	f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
   397  	if err != nil {
   398  		return err
   399  	}
   400  	var changed bool
   401  	for _, imp := range f.Imports {
   402  		impPath, err := strconv.Unquote(imp.Path.Value)
   403  		if err != nil {
   404  			log.Printf("%s: bad import spec %q: %v",
   405  				fset.Position(imp.Pos()), imp.Path.Value, err)
   406  			continue
   407  		}
   408  		canon, ok := canonical[impPath]
   409  		if !ok {
   410  			continue // import path is canonical
   411  		}
   412  
   413  		used[impPath] = true
   414  
   415  		if canon.path == "" {
   416  			// The canonical path is unknown (a -baddomain).
   417  			// Show the offending import.
   418  			// TODO(adonovan): should we show the actual source text?
   419  			fmt.Fprintf(stderr, "\t%s:%d: import %q\n",
   420  				shortPath(filename),
   421  				fset.Position(imp.Pos()).Line, impPath)
   422  			continue
   423  		}
   424  
   425  		changed = true
   426  
   427  		imp.Path.Value = strconv.Quote(canon.path)
   428  
   429  		// Add a renaming import if necessary.
   430  		//
   431  		// This is a guess at best.  We can't see whether a 'go
   432  		// get' of the canonical import path would have the same
   433  		// name or not.  Assume it's the last segment.
   434  		newBase := path.Base(canon.path)
   435  		if imp.Name == nil && newBase != canon.name {
   436  			imp.Name = &ast.Ident{Name: canon.name}
   437  		}
   438  	}
   439  
   440  	if changed && !*dryrun {
   441  		var buf bytes.Buffer
   442  		if err := format.Node(&buf, fset, f); err != nil {
   443  			return fmt.Errorf("%s: couldn't format file: %v", filename, err)
   444  		}
   445  		return writeFile(filename, buf.Bytes(), 0644)
   446  	}
   447  
   448  	return nil
   449  }
   450  
   451  // listPackage corresponds to the output of go list -json,
   452  // but only the fields we need.
   453  type listPackage struct {
   454  	Name          string
   455  	Dir           string
   456  	ImportPath    string
   457  	GoFiles       []string
   458  	TestGoFiles   []string
   459  	XTestGoFiles  []string
   460  	Imports       []string
   461  	TestImports   []string
   462  	XTestImports  []string
   463  	ImportComment string
   464  	Error         *packageError // error loading package
   465  }
   466  
   467  // A packageError describes an error loading information about a package.
   468  type packageError struct {
   469  	ImportStack []string // shortest path from package named on command line to this one
   470  	Pos         string   // position of error
   471  	Err         string   // the error itself
   472  }
   473  
   474  func (e packageError) Error() string {
   475  	if e.Pos != "" {
   476  		return e.Pos + ": " + e.Err
   477  	}
   478  	return e.Err
   479  }
   480  
   481  // list runs 'go list' with the specified arguments and returns the
   482  // metadata for matching packages.
   483  func list(args ...string) ([]*listPackage, error) {
   484  	cmd := exec.Command("go", append([]string{"list", "-e", "-json"}, args...)...)
   485  	cmd.Stdout = new(bytes.Buffer)
   486  	cmd.Stderr = stderr
   487  	if err := cmd.Run(); err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	dec := json.NewDecoder(cmd.Stdout.(io.Reader))
   492  	var pkgs []*listPackage
   493  	for {
   494  		var p listPackage
   495  		if err := dec.Decode(&p); err == io.EOF {
   496  			break
   497  		} else if err != nil {
   498  			return nil, err
   499  		}
   500  		pkgs = append(pkgs, &p)
   501  	}
   502  	return pkgs, nil
   503  }
   504  
   505  // cwd contains the current working directory of the tool.
   506  //
   507  // It is initialized directly so that its value will be set for any other
   508  // package variables or init functions that depend on it, such as the gopath
   509  // variable in main_test.go.
   510  var cwd string = func() string {
   511  	cwd, err := os.Getwd()
   512  	if err != nil {
   513  		log.Fatalf("os.Getwd: %v", err)
   514  	}
   515  	return cwd
   516  }()
   517  
   518  // shortPath returns an absolute or relative name for path, whatever is shorter.
   519  // Plundered from $GOROOT/src/cmd/go/build.go.
   520  func shortPath(path string) string {
   521  	if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) {
   522  		return rel
   523  	}
   524  	return path
   525  }
   526  

View as plain text