mirror of
https://github.com/golang/go
synced 2024-11-18 12:14:42 -07:00
af0fde4393
+ Tests. Change-Id: I0544546dda93d24aedbbfe1ffdc6882e76bfb3f8 Reviewed-on: https://go-review.googlesource.com/12940 Reviewed-by: Jason Buberel <jbuberel@google.com>
512 lines
14 KiB
Go
512 lines
14 KiB
Go
// 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 canical 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 http://godoc.org/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.Fprintf(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, msg)
|
|
}
|
|
}
|
|
|
|
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 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var cwd string
|
|
|
|
func init() {
|
|
var err error
|
|
cwd, err = os.Getwd()
|
|
if err != nil {
|
|
log.Fatalf("os.Getwd: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|