src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }