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  }