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.
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.
12 // The Go 1 custom import path mechanism lets the maintainer of a
13 // package give it a stable name by which clients may import and "go
14 // get" it, independent of the underlying version control system (such
15 // as Git) or server (such as github.com) that hosts it. Requests for
16 // the custom name are redirected to the underlying name. This allows
17 // packages to be migrated from one underlying server or system to
18 // another without breaking existing clients.
20 // Because this redirect mechanism creates aliases for existing
21 // packages, it's possible for a single program to import the same
22 // package by its canonical name and by an alias. The resulting
23 // executable will contain two copies of the package, which is wasteful
24 // at best and incorrect at worst.
26 // To avoid this, "go build" reports an error if it encounters a special
27 // comment like the one below, and if the import path in the comment
28 // does not match the path of the enclosing package relative to
31 // $ grep ^package $GOPATH/src/github.com/bob/vanity/foo/foo.go
32 // package foo // import "vanity.com/foo"
34 // The error from "go build" indicates that the package canonically
35 // known as "vanity.com/foo" is locally installed under the
36 // non-canonical name "github.com/bob/vanity/foo".
41 // When a package that you depend on introduces a custom import comment,
42 // and your workspace imports it by the non-canonical name, your build
43 // will stop working as soon as you update your copy of that package
46 // The purpose of the fiximports tool is to fix up all imports of the
47 // non-canonical path within a Go workspace, replacing them with imports
48 // of the canonical path. Following a run of fiximports, the workspace
49 // will no longer depend on the non-canonical copy of the package, so it
50 // should be safe to delete. It may be necessary to run "go get -u"
51 // again to ensure that the package is locally installed under its
52 // canonical path, if it was not already.
54 // The fiximports tool operates locally; it does not make HTTP requests
55 // and does not discover new custom import comments. It only operates
56 // on non-canonical packages present in your workspace.
58 // The -baddomains flag is a list of domain names that should always be
59 // considered non-canonical. You can use this if you wish to make sure
60 // that you no longer have any dependencies on packages from that
61 // domain, even those that do not yet provide a canonical import path
62 // comment. For example, the default value of -baddomains includes the
63 // moribund code hosting site code.google.com, so fiximports will report
64 // an error for each import of a package from this domain remaining
65 // after canonicalization.
67 // To see the changes fiximports would make without applying them, use
96 dryrun = flag.Bool("n", false, "dry run: show changes, but don't apply them")
97 badDomains = flag.String("baddomains", "code.google.com",
98 "a comma-separated list of domains from which packages should not be imported")
99 replaceFlag = flag.String("replace", "",
100 "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.")
105 stderr io.Writer = os.Stderr
106 writeFile = ioutil.WriteFile
109 const usage = `fiximports: rewrite import paths to use canonical package names.
111 Usage: fiximports [-n] package...
113 The package... arguments specify a list of packages
114 in the style of the go tool; see "go help packages".
115 Hint: use "all" or "..." to match the entire workspace.
117 For details, see https://pkg.go.dev/golang.org/x/tools/cmd/fiximports
120 -n: dry run: show changes, but don't apply them
121 -baddomains a comma-separated list of domains from which packages
122 should not be imported
128 if len(flag.Args()) == 0 {
129 fmt.Fprint(stderr, usage)
132 if !fiximports(flag.Args()...) {
137 type canonicalName struct{ path, name string }
139 // fiximports fixes imports in the specified packages.
140 // Invariant: a false result implies an error was already printed.
141 func fiximports(packages ...string) bool {
142 // importedBy is the transpose of the package import graph.
143 importedBy := make(map[string]map[*build.Package]bool)
145 // addEdge adds an edge to the import graph.
146 addEdge := func(from *build.Package, to string) {
147 if to == "C" || to == "unsafe" {
150 pkgs := importedBy[to]
152 pkgs = make(map[*build.Package]bool)
153 importedBy[to] = pkgs
158 // List metadata for all packages in the workspace.
159 pkgs, err := list("...")
161 fmt.Fprintf(stderr, "importfix: %v\n", err)
165 // packageName maps each package's path to its name.
166 packageName := make(map[string]string)
167 for _, p := range pkgs {
168 packageName[p.ImportPath] = p.Package.Name
171 // canonical maps each non-canonical package path to
172 // its canonical path and name.
173 // A present nil value indicates that the canonical package
174 // is unknown: hosted on a bad domain with no redirect.
175 canonical := make(map[string]canonicalName)
176 domains := strings.Split(*badDomains, ",")
178 type replaceItem struct {
182 var replace []replaceItem
183 for _, pair := range strings.Split(*replaceFlag, ",") {
187 words := strings.Split(pair, "=")
189 fmt.Fprintf(stderr, "importfix: -replace: %q is not of the form \"canonical=noncanonical\".\n", pair)
192 replace = append(replace, replaceItem{
193 old: strings.TrimSuffix(words[0], "..."),
194 new: strings.TrimSuffix(words[1], "..."),
195 matchPrefix: strings.HasSuffix(words[0], "...") &&
196 strings.HasSuffix(words[1], "..."),
200 // Find non-canonical packages and populate importedBy graph.
201 for _, p := range pkgs {
204 if strings.Contains(msg, "code in directory") &&
205 strings.Contains(msg, "expects import") {
206 // don't show the very errors we're trying to fix
208 fmt.Fprintln(stderr, p.Error)
212 for _, imp := range p.Imports {
213 addEdge(&p.Package, imp)
215 for _, imp := range p.TestImports {
216 addEdge(&p.Package, imp)
218 for _, imp := range p.XTestImports {
219 addEdge(&p.Package, imp)
222 // Does package have an explicit import comment?
223 if p.ImportComment != "" {
224 if p.ImportComment != p.ImportPath {
225 canonical[p.ImportPath] = canonicalName{
226 path: p.Package.ImportComment,
227 name: p.Package.Name,
231 // Is package matched by a -replace item?
233 for _, item := range replace {
234 if item.matchPrefix {
235 if strings.HasPrefix(p.ImportPath, item.old) {
236 newPath = item.new + p.ImportPath[len(item.old):]
239 } else if p.ImportPath == item.old {
245 newName := packageName[newPath]
247 newName = filepath.Base(newPath) // a guess
249 canonical[p.ImportPath] = canonicalName{
256 // Is package matched by a -baddomains item?
257 for _, domain := range domains {
258 slash := strings.Index(p.ImportPath, "/")
260 continue // no slash: standard package
262 if p.ImportPath[:slash] == domain {
263 // Package comes from bad domain and has no import comment.
264 // Report an error each time this package is imported.
265 canonical[p.ImportPath] = canonicalName{}
267 // TODO(adonovan): should we make an HTTP request to
268 // see if there's an HTTP redirect, a "go-import" meta tag,
269 // or an import comment in the latest revision?
270 // It would duplicate a lot of logic from "go get".
277 // Find all clients (direct importers) of canonical packages.
278 // These are the packages that need fixing up.
279 clients := make(map[*build.Package]bool)
280 for path := range canonical {
281 for client := range importedBy[path] {
282 clients[client] = true
286 // Restrict rewrites to the set of packages specified by the user.
287 if len(packages) == 1 && (packages[0] == "all" || packages[0] == "...") {
290 pkgs, err := list(packages...)
292 fmt.Fprintf(stderr, "importfix: %v\n", err)
295 seen := make(map[string]bool)
296 for _, p := range pkgs {
297 seen[p.ImportPath] = true
299 for client := range clients {
300 if !seen[client.ImportPath] {
301 delete(clients, client)
306 // Rewrite selected client packages.
308 for client := range clients {
309 if !rewritePackage(client, canonical) {
312 // There were errors.
313 // Show direct and indirect imports of client.
314 seen := make(map[string]bool)
315 var direct, indirect []string
316 for p := range importedBy[client.ImportPath] {
317 direct = append(direct, p.ImportPath)
318 seen[p.ImportPath] = true
321 var visit func(path string)
322 visit = func(path string) {
323 for q := range importedBy[path] {
324 qpath := q.ImportPath
327 indirect = append(indirect, qpath)
334 fmt.Fprintf(stderr, "\timported directly by:\n")
336 for _, path := range direct {
337 fmt.Fprintf(stderr, "\t\t%s\n", path)
342 fmt.Fprintf(stderr, "\timported indirectly by:\n")
343 sort.Strings(indirect)
344 for _, path := range indirect {
345 fmt.Fprintf(stderr, "\t\t%s\n", path)
355 // Invariant: false result => error already printed.
356 func rewritePackage(client *build.Package, canonical map[string]canonicalName) bool {
359 used := make(map[string]bool)
360 var filenames []string
361 filenames = append(filenames, client.GoFiles...)
362 filenames = append(filenames, client.TestGoFiles...)
363 filenames = append(filenames, client.XTestGoFiles...)
365 for _, filename := range filenames {
368 fmt.Fprintf(stderr, "%s\n", client.ImportPath)
370 err := rewriteFile(filepath.Join(client.Dir, filename), canonical, used)
372 fmt.Fprintf(stderr, "\tERROR: %v\n", err)
377 // Show which imports were renamed in this package.
379 for key := range used {
380 keys = append(keys, key)
383 for _, key := range keys {
384 if p := canonical[key]; p.path != "" {
385 fmt.Fprintf(stderr, "\tfixed: %s -> %s\n", key, p.path)
387 fmt.Fprintf(stderr, "\tERROR: %s has no import comment\n", key)
395 // rewrite reads, modifies, and writes filename, replacing all imports
396 // of packages P in canonical by canonical[P].
397 // It records in used which canonical packages were imported.
398 // used[P]=="" indicates that P was imported but its canonical path is unknown.
399 func rewriteFile(filename string, canonical map[string]canonicalName, used map[string]bool) error {
400 fset := token.NewFileSet()
401 f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
406 for _, imp := range f.Imports {
407 impPath, err := strconv.Unquote(imp.Path.Value)
409 log.Printf("%s: bad import spec %q: %v",
410 fset.Position(imp.Pos()), imp.Path.Value, err)
413 canon, ok := canonical[impPath]
415 continue // import path is canonical
420 if canon.path == "" {
421 // The canonical path is unknown (a -baddomain).
422 // Show the offending import.
423 // TODO(adonovan): should we show the actual source text?
424 fmt.Fprintf(stderr, "\t%s:%d: import %q\n",
426 fset.Position(imp.Pos()).Line, impPath)
432 imp.Path.Value = strconv.Quote(canon.path)
434 // Add a renaming import if necessary.
436 // This is a guess at best. We can't see whether a 'go
437 // get' of the canonical import path would have the same
438 // name or not. Assume it's the last segment.
439 newBase := path.Base(canon.path)
440 if imp.Name == nil && newBase != canon.name {
441 imp.Name = &ast.Ident{Name: canon.name}
445 if changed && !*dryrun {
447 if err := format.Node(&buf, fset, f); err != nil {
448 return fmt.Errorf("%s: couldn't format file: %v", filename, err)
450 return writeFile(filename, buf.Bytes(), 0644)
456 // listPackage is a copy of cmd/go/list.Package.
457 // It has more fields than build.Package and we need some of them.
458 type listPackage struct {
460 Error *packageError // error loading package
463 // A packageError describes an error loading information about a package.
464 type packageError struct {
465 ImportStack []string // shortest path from package named on command line to this one
466 Pos string // position of error
467 Err string // the error itself
470 func (e packageError) Error() string {
472 return e.Pos + ": " + e.Err
477 // list runs 'go list' with the specified arguments and returns the
478 // metadata for matching packages.
479 func list(args ...string) ([]*listPackage, error) {
480 cmd := exec.Command("go", append([]string{"list", "-e", "-json"}, args...)...)
481 cmd.Stdout = new(bytes.Buffer)
483 if err := cmd.Run(); err != nil {
487 dec := json.NewDecoder(cmd.Stdout.(io.Reader))
488 var pkgs []*listPackage
491 if err := dec.Decode(&p); err == io.EOF {
493 } else if err != nil {
496 pkgs = append(pkgs, &p)
501 // cwd contains the current working directory of the tool.
503 // It is initialized directly so that its value will be set for any other
504 // package variables or init functions that depend on it, such as the gopath
505 // variable in main_test.go.
506 var cwd string = func() string {
507 cwd, err := os.Getwd()
509 log.Fatalf("os.Getwd: %v", err)
514 // shortPath returns an absolute or relative name for path, whatever is shorter.
515 // Plundered from $GOROOT/src/cmd/go/build.go.
516 func shortPath(path string) string {
517 if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) {