GoPLS Viewer

Home|gopls/cmd/fiximports/main.go
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.
67package main
68
69import (
70    "bytes"
71    "encoding/json"
72    "flag"
73    "fmt"
74    "go/ast"
75    "go/build"
76    "go/format"
77    "go/parser"
78    "go/token"
79    exec "golang.org/x/sys/execabs"
80    "io"
81    "io/ioutil"
82    "log"
83    "os"
84    "path"
85    "path/filepath"
86    "sort"
87    "strconv"
88    "strings"
89)
90
91// flags
92var (
93    dryrun     = flag.Bool("n"false"dry run: show changes, but don't apply them")
94    badDomains = flag.String("baddomains""code.google.com",
95        "a comma-separated list of domains from which packages should not be imported")
96    replaceFlag = flag.String("replace""",
97        "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.")
98)
99
100// seams for testing
101var (
102    stderr    io.Writer = os.Stderr
103    writeFile           = ioutil.WriteFile
104)
105
106const usage = `fiximports: rewrite import paths to use canonical package names.
107
108Usage: fiximports [-n] package...
109
110The package... arguments specify a list of packages
111in the style of the go tool; see "go help packages".
112Hint: use "all" or "..." to match the entire workspace.
113
114For details, see https://pkg.go.dev/golang.org/x/tools/cmd/fiximports
115
116Flags:
117  -n:           dry run: show changes, but don't apply them
118  -baddomains  a comma-separated list of domains from which packages
119               should not be imported
120`
121
122func main() {
123    flag.Parse()
124
125    if len(flag.Args()) == 0 {
126        fmt.Fprint(stderrusage)
127        os.Exit(1)
128    }
129    if !fiximports(flag.Args()...) {
130        os.Exit(1)
131    }
132}
133
134type canonicalName struct{ pathname string }
135
136// fiximports fixes imports in the specified packages.
137// Invariant: a false result implies an error was already printed.
138func fiximports(packages ...stringbool {
139    // importedBy is the transpose of the package import graph.
140    importedBy := make(map[string]map[*build.Package]bool)
141
142    // addEdge adds an edge to the import graph.
143    addEdge := func(from *build.Packageto string) {
144        if to == "C" || to == "unsafe" {
145            return // fake
146        }
147        pkgs := importedBy[to]
148        if pkgs == nil {
149            pkgs = make(map[*build.Package]bool)
150            importedBy[to] = pkgs
151        }
152        pkgs[from] = true
153    }
154
155    // List metadata for all packages in the workspace.
156    pkgserr := list("...")
157    if err != nil {
158        fmt.Fprintf(stderr"importfix: %v\n"err)
159        return false
160    }
161
162    // packageName maps each package's path to its name.
163    packageName := make(map[string]string)
164    for _p := range pkgs {
165        packageName[p.ImportPath] = p.Package.Name
166    }
167
168    // canonical maps each non-canonical package path to
169    // its canonical path and name.
170    // A present nil value indicates that the canonical package
171    // is unknown: hosted on a bad domain with no redirect.
172    canonical := make(map[string]canonicalName)
173    domains := strings.Split(*badDomains",")
174
175    type replaceItem struct {
176        oldnew    string
177        matchPrefix bool
178    }
179    var replace []replaceItem
180    for _pair := range strings.Split(*replaceFlag",") {
181        if pair == "" {
182            continue
183        }
184        words := strings.Split(pair"=")
185        if len(words) != 2 {
186            fmt.Fprintf(stderr"importfix: -replace: %q is not of the form \"canonical=noncanonical\".\n"pair)
187            return false
188        }
189        replace = append(replacereplaceItem{
190            oldstrings.TrimSuffix(words[0], "..."),
191            newstrings.TrimSuffix(words[1], "..."),
192            matchPrefixstrings.HasSuffix(words[0], "...") &&
193                strings.HasSuffix(words[1], "..."),
194        })
195    }
196
197    // Find non-canonical packages and populate importedBy graph.
198    for _p := range pkgs {
199        if p.Error != nil {
200            msg := p.Error.Err
201            if strings.Contains(msg"code in directory") &&
202                strings.Contains(msg"expects import") {
203                // don't show the very errors we're trying to fix
204            } else {
205                fmt.Fprintln(stderrp.Error)
206            }
207        }
208
209        for _imp := range p.Imports {
210            addEdge(&p.Packageimp)
211        }
212        for _imp := range p.TestImports {
213            addEdge(&p.Packageimp)
214        }
215        for _imp := range p.XTestImports {
216            addEdge(&p.Packageimp)
217        }
218
219        // Does package have an explicit import comment?
220        if p.ImportComment != "" {
221            if p.ImportComment != p.ImportPath {
222                canonical[p.ImportPath] = canonicalName{
223                    pathp.Package.ImportComment,
224                    namep.Package.Name,
225                }
226            }
227        } else {
228            // Is package matched by a -replace item?
229            var newPath string
230            for _item := range replace {
231                if item.matchPrefix {
232                    if strings.HasPrefix(p.ImportPathitem.old) {
233                        newPath = item.new + p.ImportPath[len(item.old):]
234                        break
235                    }
236                } else if p.ImportPath == item.old {
237                    newPath = item.new
238                    break
239                }
240            }
241            if newPath != "" {
242                newName := packageName[newPath]
243                if newName == "" {
244                    newName = filepath.Base(newPath// a guess
245                }
246                canonical[p.ImportPath] = canonicalName{
247                    pathnewPath,
248                    namenewName,
249                }
250                continue
251            }
252
253            // Is package matched by a -baddomains item?
254            for _domain := range domains {
255                slash := strings.Index(p.ImportPath"/")
256                if slash < 0 {
257                    continue // no slash: standard package
258                }
259                if p.ImportPath[:slash] == domain {
260                    // Package comes from bad domain and has no import comment.
261                    // Report an error each time this package is imported.
262                    canonical[p.ImportPath] = canonicalName{}
263
264                    // TODO(adonovan): should we make an HTTP request to
265                    // see if there's an HTTP redirect, a "go-import" meta tag,
266                    // or an import comment in the latest revision?
267                    // It would duplicate a lot of logic from "go get".
268                }
269                break
270            }
271        }
272    }
273
274    // Find all clients (direct importers) of canonical packages.
275    // These are the packages that need fixing up.
276    clients := make(map[*build.Package]bool)
277    for path := range canonical {
278        for client := range importedBy[path] {
279            clients[client] = true
280        }
281    }
282
283    // Restrict rewrites to the set of packages specified by the user.
284    if len(packages) == 1 && (packages[0] == "all" || packages[0] == "...") {
285        // no restriction
286    } else {
287        pkgserr := list(packages...)
288        if err != nil {
289            fmt.Fprintf(stderr"importfix: %v\n"err)
290            return false
291        }
292        seen := make(map[string]bool)
293        for _p := range pkgs {
294            seen[p.ImportPath] = true
295        }
296        for client := range clients {
297            if !seen[client.ImportPath] {
298                delete(clientsclient)
299            }
300        }
301    }
302
303    // Rewrite selected client packages.
304    ok := true
305    for client := range clients {
306        if !rewritePackage(clientcanonical) {
307            ok = false
308
309            // There were errors.
310            // Show direct and indirect imports of client.
311            seen := make(map[string]bool)
312            var directindirect []string
313            for p := range importedBy[client.ImportPath] {
314                direct = append(directp.ImportPath)
315                seen[p.ImportPath] = true
316            }
317
318            var visit func(path string)
319            visit = func(path string) {
320                for q := range importedBy[path] {
321                    qpath := q.ImportPath
322                    if !seen[qpath] {
323                        seen[qpath] = true
324                        indirect = append(indirectqpath)
325                        visit(qpath)
326                    }
327                }
328            }
329
330            if direct != nil {
331                fmt.Fprintf(stderr"\timported directly by:\n")
332                sort.Strings(direct)
333                for _path := range direct {
334                    fmt.Fprintf(stderr"\t\t%s\n"path)
335                    visit(path)
336                }
337
338                if indirect != nil {
339                    fmt.Fprintf(stderr"\timported indirectly by:\n")
340                    sort.Strings(indirect)
341                    for _path := range indirect {
342                        fmt.Fprintf(stderr"\t\t%s\n"path)
343                    }
344                }
345            }
346        }
347    }
348
349    return ok
350}
351
352// Invariant: false result => error already printed.
353func rewritePackage(client *build.Packagecanonical map[string]canonicalNamebool {
354    ok := true
355
356    used := make(map[string]bool)
357    var filenames []string
358    filenames = append(filenamesclient.GoFiles...)
359    filenames = append(filenamesclient.TestGoFiles...)
360    filenames = append(filenamesclient.XTestGoFiles...)
361    var first bool
362    for _filename := range filenames {
363        if !first {
364            first = true
365            fmt.Fprintf(stderr"%s\n"client.ImportPath)
366        }
367        err := rewriteFile(filepath.Join(client.Dirfilename), canonicalused)
368        if err != nil {
369            fmt.Fprintf(stderr"\tERROR: %v\n"err)
370            ok = false
371        }
372    }
373
374    // Show which imports were renamed in this package.
375    var keys []string
376    for key := range used {
377        keys = append(keyskey)
378    }
379    sort.Strings(keys)
380    for _key := range keys {
381        if p := canonical[key]; p.path != "" {
382            fmt.Fprintf(stderr"\tfixed: %s -> %s\n"keyp.path)
383        } else {
384            fmt.Fprintf(stderr"\tERROR: %s has no import comment\n"key)
385            ok = false
386        }
387    }
388
389    return ok
390}
391
392// rewrite reads, modifies, and writes filename, replacing all imports
393// of packages P in canonical by canonical[P].
394// It records in used which canonical packages were imported.
395// used[P]=="" indicates that P was imported but its canonical path is unknown.
396func rewriteFile(filename stringcanonical map[string]canonicalNameused map[string]boolerror {
397    fset := token.NewFileSet()
398    ferr := parser.ParseFile(fsetfilenamenilparser.ParseComments)
399    if err != nil {
400        return err
401    }
402    var changed bool
403    for _imp := range f.Imports {
404        impPatherr := strconv.Unquote(imp.Path.Value)
405        if err != nil {
406            log.Printf("%s: bad import spec %q: %v",
407                fset.Position(imp.Pos()), imp.Path.Valueerr)
408            continue
409        }
410        canonok := canonical[impPath]
411        if !ok {
412            continue // import path is canonical
413        }
414
415        used[impPath] = true
416
417        if canon.path == "" {
418            // The canonical path is unknown (a -baddomain).
419            // Show the offending import.
420            // TODO(adonovan): should we show the actual source text?
421            fmt.Fprintf(stderr"\t%s:%d: import %q\n",
422                shortPath(filename),
423                fset.Position(imp.Pos()).LineimpPath)
424            continue
425        }
426
427        changed = true
428
429        imp.Path.Value = strconv.Quote(canon.path)
430
431        // Add a renaming import if necessary.
432        //
433        // This is a guess at best.  We can't see whether a 'go
434        // get' of the canonical import path would have the same
435        // name or not.  Assume it's the last segment.
436        newBase := path.Base(canon.path)
437        if imp.Name == nil && newBase != canon.name {
438            imp.Name = &ast.Ident{Namecanon.name}
439        }
440    }
441
442    if changed && !*dryrun {
443        var buf bytes.Buffer
444        if err := format.Node(&buffsetf); err != nil {
445            return fmt.Errorf("%s: couldn't format file: %v"filenameerr)
446        }
447        return writeFile(filenamebuf.Bytes(), 0644)
448    }
449
450    return nil
451}
452
453// listPackage is a copy of cmd/go/list.Package.
454// It has more fields than build.Package and we need some of them.
455type listPackage struct {
456    build.Package
457    Error *packageError // error loading package
458}
459
460// A packageError describes an error loading information about a package.
461type packageError struct {
462    ImportStack []string // shortest path from package named on command line to this one
463    Pos         string   // position of error
464    Err         string   // the error itself
465}
466
467func (e packageErrorError() string {
468    if e.Pos != "" {
469        return e.Pos + ": " + e.Err
470    }
471    return e.Err
472}
473
474// list runs 'go list' with the specified arguments and returns the
475// metadata for matching packages.
476func list(args ...string) ([]*listPackageerror) {
477    cmd := exec.Command("go"append([]string{"list""-e""-json"}, args...)...)
478    cmd.Stdout = new(bytes.Buffer)
479    cmd.Stderr = stderr
480    if err := cmd.Run(); err != nil {
481        return nilerr
482    }
483
484    dec := json.NewDecoder(cmd.Stdout.(io.Reader))
485    var pkgs []*listPackage
486    for {
487        var p listPackage
488        if err := dec.Decode(&p); err == io.EOF {
489            break
490        } else if err != nil {
491            return nilerr
492        }
493        pkgs = append(pkgs, &p)
494    }
495    return pkgsnil
496}
497
498// cwd contains the current working directory of the tool.
499//
500// It is initialized directly so that its value will be set for any other
501// package variables or init functions that depend on it, such as the gopath
502// variable in main_test.go.
503var cwd string = func() string {
504    cwderr := os.Getwd()
505    if err != nil {
506        log.Fatalf("os.Getwd: %v"err)
507    }
508    return cwd
509}()
510
511// shortPath returns an absolute or relative name for path, whatever is shorter.
512// Plundered from $GOROOT/src/cmd/go/build.go.
513func shortPath(path stringstring {
514    if relerr := filepath.Rel(cwdpath); err == nil && len(rel) < len(path) {
515        return rel
516    }
517    return path
518}
519
MembersX
token
fiximports.RangeStmt_6576.BlockStmt.RangeStmt_6921.imp
list
list.BlockStmt.err
err
fiximports.RangeStmt_8726.BlockStmt.RangeStmt_8758.client
list.args
stderr
packageError.Error
strconv
fiximports.replace
rewritePackage.ok
rewriteFile.filename
rewriteFile.RangeStmt_11976.imp
main
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.indirect
rewritePackage.RangeStmt_11224.key
packageError.Pos
writeFile
listPackage
fiximports.RangeStmt_6576.BlockStmt.BlockStmt.RangeStmt_7949.BlockStmt.slash
rewriteFile.fset
io
strings
rewriteFile.f
list.BlockStmt.p
fiximports.pkgs
fiximports.replaceItem.new
fiximports.RangeStmt_6576.BlockStmt.RangeStmt_6991.imp
rewritePackage.filenames
rewritePackage.RangeStmt_11149.key
build
parser
fiximports.RangeStmt_6004.BlockStmt.words
rewriteFile.RangeStmt_11976.BlockStmt.newBase
cwd
flag
ioutil
rewritePackage.RangeStmt_10795.BlockStmt.err
rewritePackage
rewritePackage.first
list.dec
sort
fiximports.importedBy
fiximports.RangeStmt_5537.p
fiximports.RangeStmt_6576.BlockStmt.RangeStmt_6855.imp
rewriteFile.canonical
usage
fiximports.replaceItem.matchPrefix
rewriteFile.BlockStmt.err
shortPath.rel
os
rewritePackage.used
rewritePackage.keys
list.cmd
list.err
fiximports
fiximports.replaceItem.old
fiximports.RangeStmt_6576.BlockStmt.BlockStmt.RangeStmt_7949.domain
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.seen
rewriteFile.RangeStmt_11976.BlockStmt.err
canonicalName
rewritePackage.canonical
shortPath
shortPath.err
path
rewriteFile.err
packageError
packageError.Error.e
list.pkgs
exec
fiximports.err
fiximports.RangeStmt_9371.client
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.BlockStmt.BlockStmt.RangeStmt_10296.path
rewriteFile.BlockStmt.buf
fiximports.RangeStmt_6576.BlockStmt.BlockStmt.RangeStmt_7381.item
rewritePackage.client
packageError.Err
json
fiximports.canonical
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.direct
shortPath.path
canonicalName.name
fiximports.replaceItem
rewriteFile.used
fiximports.RangeStmt_8726.path
rewritePackage.RangeStmt_10795.filename
fiximports.BlockStmt.RangeStmt_9216.client
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.RangeStmt_9602.p
rewriteFile.changed
format
filepath
canonicalName.path
fiximports.RangeStmt_6004.pair
fiximports.BlockStmt.RangeStmt_9156.p
rewriteFile.RangeStmt_11976.BlockStmt.impPath
listPackage.Error
packageError.ImportStack
log
fiximports.packages
fiximports.domains
fiximports.BlockStmt.pkgs
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.BlockStmt.RangeStmt_9793.BlockStmt.qpath
fiximports.ok
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.BlockStmt.RangeStmt_9793.q
fmt
fiximports.BlockStmt.seen
fiximports.RangeStmt_9371.BlockStmt.BlockStmt.BlockStmt.RangeStmt_10085.path
fiximports.packageName
fiximports.RangeStmt_6576.BlockStmt.BlockStmt.msg
fiximports.BlockStmt.err
rewriteFile
bytes
ast
fiximports.RangeStmt_6576.p
fiximports.RangeStmt_6576.BlockStmt.BlockStmt.newPath
fiximports.clients
Members
X