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