github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/cmd/goimports/goimports.go (about) 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 "github.com/powerman/golang-tools/internal/gocommand" 25 "github.com/powerman/golang-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 }