| 1 | // Copyright 2013 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 | package main |
| 6 | |
| 7 | import ( |
| 8 | "bufio" |
| 9 | "bytes" |
| 10 | "errors" |
| 11 | "flag" |
| 12 | "fmt" |
| 13 | "go/scanner" |
| 14 | exec "golang.org/x/sys/execabs" |
| 15 | "io" |
| 16 | "io/ioutil" |
| 17 | "log" |
| 18 | "os" |
| 19 | "path/filepath" |
| 20 | "runtime" |
| 21 | "runtime/pprof" |
| 22 | "strings" |
| 23 | |
| 24 | "golang.org/x/tools/internal/gocommand" |
| 25 | "golang.org/x/tools/internal/imports" |
| 26 | ) |
| 27 | |
| 28 | var ( |
| 29 | // main operation modes |
| 30 | list = flag.Bool("l", false, "list files whose formatting differs from goimport's") |
| 31 | write = flag.Bool("w", false, "write result to (source) file instead of stdout") |
| 32 | doDiff = flag.Bool("d", false, "display diffs instead of rewriting files") |
| 33 | srcdir = flag.String("srcdir", "", "choose imports as if source code is from `dir`. When operating on a single file, dir may instead be the complete file name.") |
| 34 | |
| 35 | verbose bool // verbose logging |
| 36 | |
| 37 | cpuProfile = flag.String("cpuprofile", "", "CPU profile output") |
| 38 | memProfile = flag.String("memprofile", "", "memory profile output") |
| 39 | memProfileRate = flag.Int("memrate", 0, "if > 0, sets runtime.MemProfileRate") |
| 40 | |
| 41 | options = &imports.Options{ |
| 42 | TabWidth: 8, |
| 43 | TabIndent: true, |
| 44 | Comments: true, |
| 45 | Fragment: true, |
| 46 | Env: &imports.ProcessEnv{ |
| 47 | GocmdRunner: &gocommand.Runner{}, |
| 48 | }, |
| 49 | } |
| 50 | exitCode = 0 |
| 51 | ) |
| 52 | |
| 53 | func init() { |
| 54 | flag.BoolVar(&options.AllErrors, "e", false, "report all errors (not just the first 10 on different lines)") |
| 55 | flag.StringVar(&options.LocalPrefix, "local", "", "put imports beginning with this string after 3rd-party packages; comma-separated list") |
| 56 | flag.BoolVar(&options.FormatOnly, "format-only", false, "if true, don't fix imports and only format. In this mode, goimports is effectively gofmt, with the addition that imports are grouped into sections.") |
| 57 | } |
| 58 | |
| 59 | func report(err error) { |
| 60 | scanner.PrintError(os.Stderr, err) |
| 61 | exitCode = 2 |
| 62 | } |
| 63 | |
| 64 | func usage() { |
| 65 | fmt.Fprintf(os.Stderr, "usage: goimports [flags] [path ...]\n") |
| 66 | flag.PrintDefaults() |
| 67 | os.Exit(2) |
| 68 | } |
| 69 | |
| 70 | func isGoFile(f os.FileInfo) bool { |
| 71 | // ignore non-Go files |
| 72 | name := f.Name() |
| 73 | return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go") |
| 74 | } |
| 75 | |
| 76 | // argumentType is which mode goimports was invoked as. |
| 77 | type argumentType int |
| 78 | |
| 79 | const ( |
| 80 | // fromStdin means the user is piping their source into goimports. |
| 81 | fromStdin argumentType = iota |
| 82 | |
| 83 | // singleArg is the common case from editors, when goimports is run on |
| 84 | // a single file. |
| 85 | singleArg |
| 86 | |
| 87 | // multipleArg is when the user ran "goimports file1.go file2.go" |
| 88 | // or ran goimports on a directory tree. |
| 89 | multipleArg |
| 90 | ) |
| 91 | |
| 92 | func processFile(filename string, in io.Reader, out io.Writer, argType argumentType) error { |
| 93 | opt := options |
| 94 | if argType == fromStdin { |
| 95 | nopt := *options |
| 96 | nopt.Fragment = true |
| 97 | opt = &nopt |
| 98 | } |
| 99 | |
| 100 | if in == nil { |
| 101 | f, err := os.Open(filename) |
| 102 | if err != nil { |
| 103 | return err |
| 104 | } |
| 105 | defer f.Close() |
| 106 | in = f |
| 107 | } |
| 108 | |
| 109 | src, err := ioutil.ReadAll(in) |
| 110 | if err != nil { |
| 111 | return err |
| 112 | } |
| 113 | |
| 114 | target := filename |
| 115 | if *srcdir != "" { |
| 116 | // Determine whether the provided -srcdirc is a directory or file |
| 117 | // and then use it to override the target. |
| 118 | // |
| 119 | // See https://github.com/dominikh/go-mode.el/issues/146 |
| 120 | if isFile(*srcdir) { |
| 121 | if argType == multipleArg { |
| 122 | return errors.New("-srcdir value can't be a file when passing multiple arguments or when walking directories") |
| 123 | } |
| 124 | target = *srcdir |
| 125 | } else if argType == singleArg && strings.HasSuffix(*srcdir, ".go") && !isDir(*srcdir) { |
| 126 | // For a file which doesn't exist on disk yet, but might shortly. |
| 127 | // e.g. user in editor opens $DIR/newfile.go and newfile.go doesn't yet exist on disk. |
| 128 | // The goimports on-save hook writes the buffer to a temp file |
| 129 | // first and runs goimports before the actual save to newfile.go. |
| 130 | // The editor's buffer is named "newfile.go" so that is passed to goimports as: |
| 131 | // goimports -srcdir=/gopath/src/pkg/newfile.go /tmp/gofmtXXXXXXXX.go |
| 132 | // and then the editor reloads the result from the tmp file and writes |
| 133 | // it to newfile.go. |
| 134 | target = *srcdir |
| 135 | } else { |
| 136 | // Pretend that file is from *srcdir in order to decide |
| 137 | // visible imports correctly. |
| 138 | target = filepath.Join(*srcdir, filepath.Base(filename)) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | res, err := imports.Process(target, src, opt) |
| 143 | if err != nil { |
| 144 | return err |
| 145 | } |
| 146 | |
| 147 | if !bytes.Equal(src, res) { |
| 148 | // formatting has changed |
| 149 | if *list { |
| 150 | fmt.Fprintln(out, filename) |
| 151 | } |
| 152 | if *write { |
| 153 | if argType == fromStdin { |
| 154 | // filename is "<standard input>" |
| 155 | return errors.New("can't use -w on stdin") |
| 156 | } |
| 157 | // On Windows, we need to re-set the permissions from the file. See golang/go#38225. |
| 158 | var perms os.FileMode |
| 159 | if fi, err := os.Stat(filename); err == nil { |
| 160 | perms = fi.Mode() & os.ModePerm |
| 161 | } |
| 162 | err = ioutil.WriteFile(filename, res, perms) |
| 163 | if err != nil { |
| 164 | return err |
| 165 | } |
| 166 | } |
| 167 | if *doDiff { |
| 168 | if argType == fromStdin { |
| 169 | filename = "stdin.go" // because <standard input>.orig looks silly |
| 170 | } |
| 171 | data, err := diff(src, res, filename) |
| 172 | if err != nil { |
| 173 | return fmt.Errorf("computing diff: %s", err) |
| 174 | } |
| 175 | fmt.Printf("diff -u %s %s\n", filepath.ToSlash(filename+".orig"), filepath.ToSlash(filename)) |
| 176 | out.Write(data) |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | if !*list && !*write && !*doDiff { |
| 181 | _, err = out.Write(res) |
| 182 | } |
| 183 | |
| 184 | return err |
| 185 | } |
| 186 | |
| 187 | func visitFile(path string, f os.FileInfo, err error) error { |
| 188 | if err == nil && isGoFile(f) { |
| 189 | err = processFile(path, nil, os.Stdout, multipleArg) |
| 190 | } |
| 191 | if err != nil { |
| 192 | report(err) |
| 193 | } |
| 194 | return nil |
| 195 | } |
| 196 | |
| 197 | func walkDir(path string) { |
| 198 | filepath.Walk(path, visitFile) |
| 199 | } |
| 200 | |
| 201 | func main() { |
| 202 | runtime.GOMAXPROCS(runtime.NumCPU()) |
| 203 | |
| 204 | // call gofmtMain in a separate function |
| 205 | // so that it can use defer and have them |
| 206 | // run before the exit. |
| 207 | gofmtMain() |
| 208 | os.Exit(exitCode) |
| 209 | } |
| 210 | |
| 211 | // parseFlags parses command line flags and returns the paths to process. |
| 212 | // It's a var so that custom implementations can replace it in other files. |
| 213 | var parseFlags = func() []string { |
| 214 | flag.BoolVar(&verbose, "v", false, "verbose logging") |
| 215 | |
| 216 | flag.Parse() |
| 217 | return flag.Args() |
| 218 | } |
| 219 | |
| 220 | func bufferedFileWriter(dest string) (w io.Writer, close func()) { |
| 221 | f, err := os.Create(dest) |
| 222 | if err != nil { |
| 223 | log.Fatal(err) |
| 224 | } |
| 225 | bw := bufio.NewWriter(f) |
| 226 | return bw, func() { |
| 227 | if err := bw.Flush(); err != nil { |
| 228 | log.Fatalf("error flushing %v: %v", dest, err) |
| 229 | } |
| 230 | if err := f.Close(); err != nil { |
| 231 | log.Fatal(err) |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | func gofmtMain() { |
| 237 | flag.Usage = usage |
| 238 | paths := parseFlags() |
| 239 | |
| 240 | if *cpuProfile != "" { |
| 241 | bw, flush := bufferedFileWriter(*cpuProfile) |
| 242 | pprof.StartCPUProfile(bw) |
| 243 | defer flush() |
| 244 | defer pprof.StopCPUProfile() |
| 245 | } |
| 246 | // doTrace is a conditionally compiled wrapper around runtime/trace. It is |
| 247 | // used to allow goimports to compile under gccgo, which does not support |
| 248 | // runtime/trace. See https://golang.org/issue/15544. |
| 249 | defer doTrace()() |
| 250 | if *memProfileRate > 0 { |
| 251 | runtime.MemProfileRate = *memProfileRate |
| 252 | bw, flush := bufferedFileWriter(*memProfile) |
| 253 | defer func() { |
| 254 | runtime.GC() // materialize all statistics |
| 255 | if err := pprof.WriteHeapProfile(bw); err != nil { |
| 256 | log.Fatal(err) |
| 257 | } |
| 258 | flush() |
| 259 | }() |
| 260 | } |
| 261 | |
| 262 | if verbose { |
| 263 | log.SetFlags(log.LstdFlags | log.Lmicroseconds) |
| 264 | options.Env.Logf = log.Printf |
| 265 | } |
| 266 | if options.TabWidth < 0 { |
| 267 | fmt.Fprintf(os.Stderr, "negative tabwidth %d\n", options.TabWidth) |
| 268 | exitCode = 2 |
| 269 | return |
| 270 | } |
| 271 | |
| 272 | if len(paths) == 0 { |
| 273 | if err := processFile("<standard input>", os.Stdin, os.Stdout, fromStdin); err != nil { |
| 274 | report(err) |
| 275 | } |
| 276 | return |
| 277 | } |
| 278 | |
| 279 | argType := singleArg |
| 280 | if len(paths) > 1 { |
| 281 | argType = multipleArg |
| 282 | } |
| 283 | |
| 284 | for _, path := range paths { |
| 285 | switch dir, err := os.Stat(path); { |
| 286 | case err != nil: |
| 287 | report(err) |
| 288 | case dir.IsDir(): |
| 289 | walkDir(path) |
| 290 | default: |
| 291 | if err := processFile(path, nil, os.Stdout, argType); err != nil { |
| 292 | report(err) |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | func writeTempFile(dir, prefix string, data []byte) (string, error) { |
| 299 | file, err := ioutil.TempFile(dir, prefix) |
| 300 | if err != nil { |
| 301 | return "", err |
| 302 | } |
| 303 | _, err = file.Write(data) |
| 304 | if err1 := file.Close(); err == nil { |
| 305 | err = err1 |
| 306 | } |
| 307 | if err != nil { |
| 308 | os.Remove(file.Name()) |
| 309 | return "", err |
| 310 | } |
| 311 | return file.Name(), nil |
| 312 | } |
| 313 | |
| 314 | func diff(b1, b2 []byte, filename string) (data []byte, err error) { |
| 315 | f1, err := writeTempFile("", "gofmt", b1) |
| 316 | if err != nil { |
| 317 | return |
| 318 | } |
| 319 | defer os.Remove(f1) |
| 320 | |
| 321 | f2, err := writeTempFile("", "gofmt", b2) |
| 322 | if err != nil { |
| 323 | return |
| 324 | } |
| 325 | defer os.Remove(f2) |
| 326 | |
| 327 | cmd := "diff" |
| 328 | if runtime.GOOS == "plan9" { |
| 329 | cmd = "/bin/ape/diff" |
| 330 | } |
| 331 | |
| 332 | data, err = exec.Command(cmd, "-u", f1, f2).CombinedOutput() |
| 333 | if len(data) > 0 { |
| 334 | // diff exits with a non-zero status when the files don't match. |
| 335 | // Ignore that failure as long as we get output. |
| 336 | return replaceTempFilename(data, filename) |
| 337 | } |
| 338 | return |
| 339 | } |
| 340 | |
| 341 | // replaceTempFilename replaces temporary filenames in diff with actual one. |
| 342 | // |
| 343 | // --- /tmp/gofmt316145376 2017-02-03 19:13:00.280468375 -0500 |
| 344 | // +++ /tmp/gofmt617882815 2017-02-03 19:13:00.280468375 -0500 |
| 345 | // ... |
| 346 | // -> |
| 347 | // --- path/to/file.go.orig 2017-02-03 19:13:00.280468375 -0500 |
| 348 | // +++ path/to/file.go 2017-02-03 19:13:00.280468375 -0500 |
| 349 | // ... |
| 350 | func replaceTempFilename(diff []byte, filename string) ([]byte, error) { |
| 351 | bs := bytes.SplitN(diff, []byte{'\n'}, 3) |
| 352 | if len(bs) < 3 { |
| 353 | return nil, fmt.Errorf("got unexpected diff for %s", filename) |
| 354 | } |
| 355 | // Preserve timestamps. |
| 356 | var t0, t1 []byte |
| 357 | if i := bytes.LastIndexByte(bs[0], '\t'); i != -1 { |
| 358 | t0 = bs[0][i:] |
| 359 | } |
| 360 | if i := bytes.LastIndexByte(bs[1], '\t'); i != -1 { |
| 361 | t1 = bs[1][i:] |
| 362 | } |
| 363 | // Always print filepath with slash separator. |
| 364 | f := filepath.ToSlash(filename) |
| 365 | bs[0] = []byte(fmt.Sprintf("--- %s%s", f+".orig", t0)) |
| 366 | bs[1] = []byte(fmt.Sprintf("+++ %s%s", f, t1)) |
| 367 | return bytes.Join(bs, []byte{'\n'}), nil |
| 368 | } |
| 369 | |
| 370 | // isFile reports whether name is a file. |
| 371 | func isFile(name string) bool { |
| 372 | fi, err := os.Stat(name) |
| 373 | return err == nil && fi.Mode().IsRegular() |
| 374 | } |
| 375 | |
| 376 | // isDir reports whether name is a directory. |
| 377 | func isDir(name string) bool { |
| 378 | fi, err := os.Stat(name) |
| 379 | return err == nil && fi.IsDir() |
| 380 | } |
| 381 |
Members