--- /dev/null
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The fiximports command fixes import declarations to use the canonical
+// import path for packages that have an "import comment" as defined by
+// https://golang.org/s/go14customimport.
+//
+//
+// Background
+//
+// The Go 1 custom import path mechanism lets the maintainer of a
+// package give it a stable name by which clients may import and "go
+// get" it, independent of the underlying version control system (such
+// as Git) or server (such as github.com) that hosts it. Requests for
+// the custom name are redirected to the underlying name. This allows
+// packages to be migrated from one underlying server or system to
+// another without breaking existing clients.
+//
+// Because this redirect mechanism creates aliases for existing
+// packages, it's possible for a single program to import the same
+// package by its canonical name and by an alias. The resulting
+// executable will contain two copies of the package, which is wasteful
+// at best and incorrect at worst.
+//
+// To avoid this, "go build" reports an error if it encounters a special
+// comment like the one below, and if the import path in the comment
+// does not match the path of the enclosing package relative to
+// GOPATH/src:
+//
+// $ grep ^package $GOPATH/src/github.com/bob/vanity/foo/foo.go
+// package foo // import "vanity.com/foo"
+//
+// The error from "go build" indicates that the package canonically
+// known as "vanity.com/foo" is locally installed under the
+// non-canonical name "github.com/bob/vanity/foo".
+//
+//
+// Usage
+//
+// When a package that you depend on introduces a custom import comment,
+// and your workspace imports it by the non-canonical name, your build
+// will stop working as soon as you update your copy of that package
+// using "go get -u".
+//
+// The purpose of the fiximports tool is to fix up all imports of the
+// non-canonical path within a Go workspace, replacing them with imports
+// of the canonical path. Following a run of fiximports, the workspace
+// will no longer depend on the non-canonical copy of the package, so it
+// should be safe to delete. It may be necessary to run "go get -u"
+// again to ensure that the package is locally installed under its
+// canonical path, if it was not already.
+//
+// The fiximports tool operates locally; it does not make HTTP requests
+// and does not discover new custom import comments. It only operates
+// on non-canonical packages present in your workspace.
+//
+// The -baddomains flag is a list of domain names that should always be
+// considered non-canonical. You can use this if you wish to make sure
+// that you no longer have any dependencies on packages from that
+// domain, even those that do not yet provide a canonical import path
+// comment. For example, the default value of -baddomains includes the
+// moribund code hosting site code.google.com, so fiximports will report
+// an error for each import of a package from this domain remaining
+// after canonicalization.
+//
+// To see the changes fiximports would make without applying them, use
+// the -n flag.
+//
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// flags
+var (
+ dryrun = flag.Bool("n", false, "dry run: show changes, but don't apply them")
+ badDomains = flag.String("baddomains", "code.google.com",
+ "a comma-separated list of domains from which packages should not be imported")
+ replaceFlag = flag.String("replace", "",
+ "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.")
+)
+
+// seams for testing
+var (
+ stderr io.Writer = os.Stderr
+ writeFile = ioutil.WriteFile
+)
+
+const usage = `fiximports: rewrite import paths to use canonical package names.
+
+Usage: fiximports [-n] package...
+
+The package... arguments specify a list of packages
+in the style of the go tool; see "go help packages".
+Hint: use "all" or "..." to match the entire workspace.
+
+For details, see https://pkg.go.dev/golang.org/x/tools/cmd/fiximports
+
+Flags:
+ -n: dry run: show changes, but don't apply them
+ -baddomains a comma-separated list of domains from which packages
+ should not be imported
+`
+
+func main() {
+ flag.Parse()
+
+ if len(flag.Args()) == 0 {
+ fmt.Fprint(stderr, usage)
+ os.Exit(1)
+ }
+ if !fiximports(flag.Args()...) {
+ os.Exit(1)
+ }
+}
+
+type canonicalName struct{ path, name string }
+
+// fiximports fixes imports in the specified packages.
+// Invariant: a false result implies an error was already printed.
+func fiximports(packages ...string) bool {
+ // importedBy is the transpose of the package import graph.
+ importedBy := make(map[string]map[*build.Package]bool)
+
+ // addEdge adds an edge to the import graph.
+ addEdge := func(from *build.Package, to string) {
+ if to == "C" || to == "unsafe" {
+ return // fake
+ }
+ pkgs := importedBy[to]
+ if pkgs == nil {
+ pkgs = make(map[*build.Package]bool)
+ importedBy[to] = pkgs
+ }
+ pkgs[from] = true
+ }
+
+ // List metadata for all packages in the workspace.
+ pkgs, err := list("...")
+ if err != nil {
+ fmt.Fprintf(stderr, "importfix: %v\n", err)
+ return false
+ }
+
+ // packageName maps each package's path to its name.
+ packageName := make(map[string]string)
+ for _, p := range pkgs {
+ packageName[p.ImportPath] = p.Package.Name
+ }
+
+ // canonical maps each non-canonical package path to
+ // its canonical path and name.
+ // A present nil value indicates that the canonical package
+ // is unknown: hosted on a bad domain with no redirect.
+ canonical := make(map[string]canonicalName)
+ domains := strings.Split(*badDomains, ",")
+
+ type replaceItem struct {
+ old, new string
+ matchPrefix bool
+ }
+ var replace []replaceItem
+ for _, pair := range strings.Split(*replaceFlag, ",") {
+ if pair == "" {
+ continue
+ }
+ words := strings.Split(pair, "=")
+ if len(words) != 2 {
+ fmt.Fprintf(stderr, "importfix: -replace: %q is not of the form \"canonical=noncanonical\".\n", pair)
+ return false
+ }
+ replace = append(replace, replaceItem{
+ old: strings.TrimSuffix(words[0], "..."),
+ new: strings.TrimSuffix(words[1], "..."),
+ matchPrefix: strings.HasSuffix(words[0], "...") &&
+ strings.HasSuffix(words[1], "..."),
+ })
+ }
+
+ // Find non-canonical packages and populate importedBy graph.
+ for _, p := range pkgs {
+ if p.Error != nil {
+ msg := p.Error.Err
+ if strings.Contains(msg, "code in directory") &&
+ strings.Contains(msg, "expects import") {
+ // don't show the very errors we're trying to fix
+ } else {
+ fmt.Fprintln(stderr, p.Error)
+ }
+ }
+
+ for _, imp := range p.Imports {
+ addEdge(&p.Package, imp)
+ }
+ for _, imp := range p.TestImports {
+ addEdge(&p.Package, imp)
+ }
+ for _, imp := range p.XTestImports {
+ addEdge(&p.Package, imp)
+ }
+
+ // Does package have an explicit import comment?
+ if p.ImportComment != "" {
+ if p.ImportComment != p.ImportPath {
+ canonical[p.ImportPath] = canonicalName{
+ path: p.Package.ImportComment,
+ name: p.Package.Name,
+ }
+ }
+ } else {
+ // Is package matched by a -replace item?
+ var newPath string
+ for _, item := range replace {
+ if item.matchPrefix {
+ if strings.HasPrefix(p.ImportPath, item.old) {
+ newPath = item.new + p.ImportPath[len(item.old):]
+ break
+ }
+ } else if p.ImportPath == item.old {
+ newPath = item.new
+ break
+ }
+ }
+ if newPath != "" {
+ newName := packageName[newPath]
+ if newName == "" {
+ newName = filepath.Base(newPath) // a guess
+ }
+ canonical[p.ImportPath] = canonicalName{
+ path: newPath,
+ name: newName,
+ }
+ continue
+ }
+
+ // Is package matched by a -baddomains item?
+ for _, domain := range domains {
+ slash := strings.Index(p.ImportPath, "/")
+ if slash < 0 {
+ continue // no slash: standard package
+ }
+ if p.ImportPath[:slash] == domain {
+ // Package comes from bad domain and has no import comment.
+ // Report an error each time this package is imported.
+ canonical[p.ImportPath] = canonicalName{}
+
+ // TODO(adonovan): should we make an HTTP request to
+ // see if there's an HTTP redirect, a "go-import" meta tag,
+ // or an import comment in the latest revision?
+ // It would duplicate a lot of logic from "go get".
+ }
+ break
+ }
+ }
+ }
+
+ // Find all clients (direct importers) of canonical packages.
+ // These are the packages that need fixing up.
+ clients := make(map[*build.Package]bool)
+ for path := range canonical {
+ for client := range importedBy[path] {
+ clients[client] = true
+ }
+ }
+
+ // Restrict rewrites to the set of packages specified by the user.
+ if len(packages) == 1 && (packages[0] == "all" || packages[0] == "...") {
+ // no restriction
+ } else {
+ pkgs, err := list(packages...)
+ if err != nil {
+ fmt.Fprintf(stderr, "importfix: %v\n", err)
+ return false
+ }
+ seen := make(map[string]bool)
+ for _, p := range pkgs {
+ seen[p.ImportPath] = true
+ }
+ for client := range clients {
+ if !seen[client.ImportPath] {
+ delete(clients, client)
+ }
+ }
+ }
+
+ // Rewrite selected client packages.
+ ok := true
+ for client := range clients {
+ if !rewritePackage(client, canonical) {
+ ok = false
+
+ // There were errors.
+ // Show direct and indirect imports of client.
+ seen := make(map[string]bool)
+ var direct, indirect []string
+ for p := range importedBy[client.ImportPath] {
+ direct = append(direct, p.ImportPath)
+ seen[p.ImportPath] = true
+ }
+
+ var visit func(path string)
+ visit = func(path string) {
+ for q := range importedBy[path] {
+ qpath := q.ImportPath
+ if !seen[qpath] {
+ seen[qpath] = true
+ indirect = append(indirect, qpath)
+ visit(qpath)
+ }
+ }
+ }
+
+ if direct != nil {
+ fmt.Fprintf(stderr, "\timported directly by:\n")
+ sort.Strings(direct)
+ for _, path := range direct {
+ fmt.Fprintf(stderr, "\t\t%s\n", path)
+ visit(path)
+ }
+
+ if indirect != nil {
+ fmt.Fprintf(stderr, "\timported indirectly by:\n")
+ sort.Strings(indirect)
+ for _, path := range indirect {
+ fmt.Fprintf(stderr, "\t\t%s\n", path)
+ }
+ }
+ }
+ }
+ }
+
+ return ok
+}
+
+// Invariant: false result => error already printed.
+func rewritePackage(client *build.Package, canonical map[string]canonicalName) bool {
+ ok := true
+
+ used := make(map[string]bool)
+ var filenames []string
+ filenames = append(filenames, client.GoFiles...)
+ filenames = append(filenames, client.TestGoFiles...)
+ filenames = append(filenames, client.XTestGoFiles...)
+ var first bool
+ for _, filename := range filenames {
+ if !first {
+ first = true
+ fmt.Fprintf(stderr, "%s\n", client.ImportPath)
+ }
+ err := rewriteFile(filepath.Join(client.Dir, filename), canonical, used)
+ if err != nil {
+ fmt.Fprintf(stderr, "\tERROR: %v\n", err)
+ ok = false
+ }
+ }
+
+ // Show which imports were renamed in this package.
+ var keys []string
+ for key := range used {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ for _, key := range keys {
+ if p := canonical[key]; p.path != "" {
+ fmt.Fprintf(stderr, "\tfixed: %s -> %s\n", key, p.path)
+ } else {
+ fmt.Fprintf(stderr, "\tERROR: %s has no import comment\n", key)
+ ok = false
+ }
+ }
+
+ return ok
+}
+
+// rewrite reads, modifies, and writes filename, replacing all imports
+// of packages P in canonical by canonical[P].
+// It records in used which canonical packages were imported.
+// used[P]=="" indicates that P was imported but its canonical path is unknown.
+func rewriteFile(filename string, canonical map[string]canonicalName, used map[string]bool) error {
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
+ if err != nil {
+ return err
+ }
+ var changed bool
+ for _, imp := range f.Imports {
+ impPath, err := strconv.Unquote(imp.Path.Value)
+ if err != nil {
+ log.Printf("%s: bad import spec %q: %v",
+ fset.Position(imp.Pos()), imp.Path.Value, err)
+ continue
+ }
+ canon, ok := canonical[impPath]
+ if !ok {
+ continue // import path is canonical
+ }
+
+ used[impPath] = true
+
+ if canon.path == "" {
+ // The canonical path is unknown (a -baddomain).
+ // Show the offending import.
+ // TODO(adonovan): should we show the actual source text?
+ fmt.Fprintf(stderr, "\t%s:%d: import %q\n",
+ shortPath(filename),
+ fset.Position(imp.Pos()).Line, impPath)
+ continue
+ }
+
+ changed = true
+
+ imp.Path.Value = strconv.Quote(canon.path)
+
+ // Add a renaming import if necessary.
+ //
+ // This is a guess at best. We can't see whether a 'go
+ // get' of the canonical import path would have the same
+ // name or not. Assume it's the last segment.
+ newBase := path.Base(canon.path)
+ if imp.Name == nil && newBase != canon.name {
+ imp.Name = &ast.Ident{Name: canon.name}
+ }
+ }
+
+ if changed && !*dryrun {
+ var buf bytes.Buffer
+ if err := format.Node(&buf, fset, f); err != nil {
+ return fmt.Errorf("%s: couldn't format file: %v", filename, err)
+ }
+ return writeFile(filename, buf.Bytes(), 0644)
+ }
+
+ return nil
+}
+
+// listPackage is a copy of cmd/go/list.Package.
+// It has more fields than build.Package and we need some of them.
+type listPackage struct {
+ build.Package
+ Error *packageError // error loading package
+}
+
+// A packageError describes an error loading information about a package.
+type packageError struct {
+ ImportStack []string // shortest path from package named on command line to this one
+ Pos string // position of error
+ Err string // the error itself
+}
+
+func (e packageError) Error() string {
+ if e.Pos != "" {
+ return e.Pos + ": " + e.Err
+ }
+ return e.Err
+}
+
+// list runs 'go list' with the specified arguments and returns the
+// metadata for matching packages.
+func list(args ...string) ([]*listPackage, error) {
+ cmd := exec.Command("go", append([]string{"list", "-e", "-json"}, args...)...)
+ cmd.Stdout = new(bytes.Buffer)
+ cmd.Stderr = stderr
+ if err := cmd.Run(); err != nil {
+ return nil, err
+ }
+
+ dec := json.NewDecoder(cmd.Stdout.(io.Reader))
+ var pkgs []*listPackage
+ for {
+ var p listPackage
+ if err := dec.Decode(&p); err == io.EOF {
+ break
+ } else if err != nil {
+ return nil, err
+ }
+ pkgs = append(pkgs, &p)
+ }
+ return pkgs, nil
+}
+
+// cwd contains the current working directory of the tool.
+//
+// It is initialized directly so that its value will be set for any other
+// package variables or init functions that depend on it, such as the gopath
+// variable in main_test.go.
+var cwd string = func() string {
+ cwd, err := os.Getwd()
+ if err != nil {
+ log.Fatalf("os.Getwd: %v", err)
+ }
+ return cwd
+}()
+
+// shortPath returns an absolute or relative name for path, whatever is shorter.
+// Plundered from $GOROOT/src/cmd/go/build.go.
+func shortPath(path string) string {
+ if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) {
+ return rel
+ }
+ return path
+}