github.com/april1989/origin-go-tools@v0.0.32/cmd/fiximports/main.go (about) 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 // 10 // Background 11 // 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. 19 // 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. 25 // 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 29 // GOPATH/src: 30 // 31 // $ grep ^package $GOPATH/src/github.com/bob/vanity/foo/foo.go 32 // package foo // import "vanity.com/foo" 33 // 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". 37 // 38 // 39 // Usage 40 // 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 44 // using "go get -u". 45 // 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. 53 // 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. 57 // 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. 66 // 67 // To see the changes fiximports would make without applying them, use 68 // the -n flag. 69 // 70 package main 71 72 import ( 73 "bytes" 74 "encoding/json" 75 "flag" 76 "fmt" 77 "go/ast" 78 "go/build" 79 "go/format" 80 "go/parser" 81 "go/token" 82 "io" 83 "io/ioutil" 84 "log" 85 "os" 86 "os/exec" 87 "path" 88 "path/filepath" 89 "sort" 90 "strconv" 91 "strings" 92 ) 93 94 // flags 95 var ( 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.") 101 ) 102 103 // seams for golibexec_testing 104 var ( 105 stderr io.Writer = os.Stderr 106 writeFile = ioutil.WriteFile 107 ) 108 109 const usage = `fiximports: rewrite import paths to use canonical package names. 110 111 Usage: fiximports [-n] package... 112 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. 116 117 For details, see http://godoc.org/github.com/april1989/origin-go-tools/cmd/fiximports. 118 119 Flags: 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 123 ` 124 125 func main() { 126 flag.Parse() 127 128 if len(flag.Args()) == 0 { 129 fmt.Fprint(stderr, usage) 130 os.Exit(1) 131 } 132 if !fiximports(flag.Args()...) { 133 os.Exit(1) 134 } 135 } 136 137 type canonicalName struct{ path, name string } 138 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) 144 145 // addEdge adds an edge to the import graph. 146 addEdge := func(from *build.Package, to string) { 147 if to == "C" || to == "unsafe" { 148 return // fake 149 } 150 pkgs := importedBy[to] 151 if pkgs == nil { 152 pkgs = make(map[*build.Package]bool) 153 importedBy[to] = pkgs 154 } 155 pkgs[from] = true 156 } 157 158 // List metadata for all packages in the workspace. 159 pkgs, err := list("...") 160 if err != nil { 161 fmt.Fprintf(stderr, "importfix: %v\n", err) 162 return false 163 } 164 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 169 } 170 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, ",") 177 178 type replaceItem struct { 179 old, new string 180 matchPrefix bool 181 } 182 var replace []replaceItem 183 for _, pair := range strings.Split(*replaceFlag, ",") { 184 if pair == "" { 185 continue 186 } 187 words := strings.Split(pair, "=") 188 if len(words) != 2 { 189 fmt.Fprintf(stderr, "importfix: -replace: %q is not of the form \"canonical=noncanonical\".\n", pair) 190 return false 191 } 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], "..."), 197 }) 198 } 199 200 // Find non-canonical packages and populate importedBy graph. 201 for _, p := range pkgs { 202 if p.Error != nil { 203 msg := p.Error.Err 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 207 } else { 208 fmt.Fprintln(stderr, p.Error) 209 } 210 } 211 212 for _, imp := range p.Imports { 213 addEdge(&p.Package, imp) 214 } 215 for _, imp := range p.TestImports { 216 addEdge(&p.Package, imp) 217 } 218 for _, imp := range p.XTestImports { 219 addEdge(&p.Package, imp) 220 } 221 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, 228 } 229 } 230 } else { 231 // Is package matched by a -replace item? 232 var newPath string 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):] 237 break 238 } 239 } else if p.ImportPath == item.old { 240 newPath = item.new 241 break 242 } 243 } 244 if newPath != "" { 245 newName := packageName[newPath] 246 if newName == "" { 247 newName = filepath.Base(newPath) // a guess 248 } 249 canonical[p.ImportPath] = canonicalName{ 250 path: newPath, 251 name: newName, 252 } 253 continue 254 } 255 256 // Is package matched by a -baddomains item? 257 for _, domain := range domains { 258 slash := strings.Index(p.ImportPath, "/") 259 if slash < 0 { 260 continue // no slash: standard package 261 } 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{} 266 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 the latest revision? 270 // It would duplicate a lot of logic from "go get". 271 } 272 break 273 } 274 } 275 } 276 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 283 } 284 } 285 286 // Restrict rewrites to the set of packages specified by the user. 287 if len(packages) == 1 && (packages[0] == "all" || packages[0] == "...") { 288 // no restriction 289 } else { 290 pkgs, err := list(packages...) 291 if err != nil { 292 fmt.Fprintf(stderr, "importfix: %v\n", err) 293 return false 294 } 295 seen := make(map[string]bool) 296 for _, p := range pkgs { 297 seen[p.ImportPath] = true 298 } 299 for client := range clients { 300 if !seen[client.ImportPath] { 301 delete(clients, client) 302 } 303 } 304 } 305 306 // Rewrite selected client packages. 307 ok := true 308 for client := range clients { 309 if !rewritePackage(client, canonical) { 310 ok = false 311 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 319 } 320 321 var visit func(path string) 322 visit = func(path string) { 323 for q := range importedBy[path] { 324 qpath := q.ImportPath 325 if !seen[qpath] { 326 seen[qpath] = true 327 indirect = append(indirect, qpath) 328 visit(qpath) 329 } 330 } 331 } 332 333 if direct != nil { 334 fmt.Fprintf(stderr, "\timported directly by:\n") 335 sort.Strings(direct) 336 for _, path := range direct { 337 fmt.Fprintf(stderr, "\t\t%s\n", path) 338 visit(path) 339 } 340 341 if indirect != nil { 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) 346 } 347 } 348 } 349 } 350 } 351 352 return ok 353 } 354 355 // Invariant: false result => error already printed. 356 func rewritePackage(client *build.Package, canonical map[string]canonicalName) bool { 357 ok := true 358 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...) 364 var first bool 365 for _, filename := range filenames { 366 if !first { 367 first = true 368 fmt.Fprintf(stderr, "%s\n", client.ImportPath) 369 } 370 err := rewriteFile(filepath.Join(client.Dir, filename), canonical, used) 371 if err != nil { 372 fmt.Fprintf(stderr, "\tERROR: %v\n", err) 373 ok = false 374 } 375 } 376 377 // Show which imports were renamed in this package. 378 var keys []string 379 for key := range used { 380 keys = append(keys, key) 381 } 382 sort.Strings(keys) 383 for _, key := range keys { 384 if p := canonical[key]; p.path != "" { 385 fmt.Fprintf(stderr, "\tfixed: %s -> %s\n", key, p.path) 386 } else { 387 fmt.Fprintf(stderr, "\tERROR: %s has no import comment\n", key) 388 ok = false 389 } 390 } 391 392 return ok 393 } 394 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) 402 if err != nil { 403 return err 404 } 405 var changed bool 406 for _, imp := range f.Imports { 407 impPath, err := strconv.Unquote(imp.Path.Value) 408 if err != nil { 409 log.Printf("%s: bad import spec %q: %v", 410 fset.Position(imp.Pos()), imp.Path.Value, err) 411 continue 412 } 413 canon, ok := canonical[impPath] 414 if !ok { 415 continue // import path is canonical 416 } 417 418 used[impPath] = true 419 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", 425 shortPath(filename), 426 fset.Position(imp.Pos()).Line, impPath) 427 continue 428 } 429 430 changed = true 431 432 imp.Path.Value = strconv.Quote(canon.path) 433 434 // Add a renaming import if necessary. 435 // 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} 442 } 443 } 444 445 if changed && !*dryrun { 446 var buf bytes.Buffer 447 if err := format.Node(&buf, fset, f); err != nil { 448 return fmt.Errorf("%s: couldn't format file: %v", filename, err) 449 } 450 return writeFile(filename, buf.Bytes(), 0644) 451 } 452 453 return nil 454 } 455 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 { 459 build.Package 460 Error *packageError // error loading package 461 } 462 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 468 } 469 470 func (e packageError) Error() string { 471 if e.Pos != "" { 472 return e.Pos + ": " + e.Err 473 } 474 return e.Err 475 } 476 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) 482 cmd.Stderr = stderr 483 if err := cmd.Run(); err != nil { 484 return nil, err 485 } 486 487 dec := json.NewDecoder(cmd.Stdout.(io.Reader)) 488 var pkgs []*listPackage 489 for { 490 var p listPackage 491 if err := dec.Decode(&p); err == io.EOF { 492 break 493 } else if err != nil { 494 return nil, err 495 } 496 pkgs = append(pkgs, &p) 497 } 498 return pkgs, nil 499 } 500 501 // cwd contains the current working directory of the tool. 502 // 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() 508 if err != nil { 509 log.Fatalf("os.Getwd: %v", err) 510 } 511 return cwd 512 }() 513 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) { 518 return rel 519 } 520 return path 521 }