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