From 318f5c0d773a5d6681c6cd6ce459cf5ab8f32bc6 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 6 May 2020 17:05:00 -0500 Subject: [PATCH] Initial commit Signed-off-by: jolheiser --- .gitignore | 5 ++ Makefile | 21 ++++++ README.md | 20 ++++++ go.mod | 11 +++ go.sum | 48 +++++++++++++ imp.go | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 36 ++++++++++ struct.go | 49 ++++++++++++++ 8 files changed, 384 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 imp.go create mode 100644 main.go create mode 100644 struct.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e8b1bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# GoLand +.idea/ + +# Binaries +/imp* \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9c54931 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +GO ?= go + +.PHONY: build +build: + $(GO) build + +.PHONY: fmt +fmt: + $(GO) fmt ./... + +.PHONY: test +test: + $(GO) test -race ./... + +.PHONY: vet +vet: + $(GO) vet ./... + +.PHONY: imp +imp: build + ./imp -w \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ac3625 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# imp + +imp is an opinionated import formatter + +The order it follows is: +```text +import ( + + + + + +) +``` + +imp includes three flags: + +* `--write` will write out the formatting rather than printing +* `--exclude` can be used multiple times to set directories to exclude +* `--verbose` will print out extended information \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c3d043 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module go.jolheiser.com/imp + +go 1.14 + +require ( + github.com/gobuffalo/here v0.6.2 + github.com/urfave/cli/v2 v2.2.0 + go.jolheiser.com/beaver v1.0.2 + go.jolheiser.com/regexp v0.1.1 + golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..636dfc1 --- /dev/null +++ b/go.sum @@ -0,0 +1,48 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gobuffalo/here v0.6.2 h1:ZtCqC7F9ou3moLbYfHM1Tj+gwHGgWhjyRjVjsir9BE0= +github.com/gobuffalo/here v0.6.2/go.mod h1:D75Sq0p2BVHdgQu3vCRsXbg85rx943V19urJpqAVWjI= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.jolheiser.com/beaver v1.0.2 h1:KA2D6iO8MQhZi1nZYi/Chak/f1Cxfrs6b1XO623+Khk= +go.jolheiser.com/beaver v1.0.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g= +go.jolheiser.com/regexp v0.1.1 h1:nMCzilEL/oIcZJwnRT4bb+FNxaeAFBdqSjq7rgoNSGg= +go.jolheiser.com/regexp v0.1.1/go.mod h1:58uCpYxGy/DbqVRuo6oU93kh3B3GeyK5eJznQvy2N6c= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vvT1btiBxyq+s0orYBqcQY= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/imp.go b/imp.go new file mode 100644 index 0000000..3a44fb5 --- /dev/null +++ b/imp.go @@ -0,0 +1,194 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/gobuffalo/here" + "github.com/urfave/cli/v2" + "go.jolheiser.com/beaver" + "go.jolheiser.com/beaver/color" + "go.jolheiser.com/regexp" +) + +var ( + module string + importRe = regexp.MustCompile(`(?ms)import \(([^)]+)\)`) + otherRe = regexp.MustCompile(`(?:var|const|func)\s`) +) + +func runImp(ctx *cli.Context) error { + if ctx.Bool("verbose") { + beaver.Console.Level = beaver.DEBUG + } + info, err := here.Current() + if err != nil { + return err + } + module = info.Module.Path + beaver.Debugf("Current module: %s", module) + + var failed bool + diffColor := color.New(color.FgGreen, color.BgHiBlack) + if err := filepath.Walk(".", func(walkPath string, walkInfo os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + + walkPath = filepath.ToSlash(walkPath) + + if s := checkSkip(walkPath, walkInfo, ctx.StringSlice("exclude")); s != CHECK { + if s == SKIPDIR { + return filepath.SkipDir + } + return nil + } + + beaver.Debugf("Checking file %s", walkPath) + data, err := ioutil.ReadFile(walkPath) + if err != nil { + return err + } + + importStart := importRe.FindIndex(data) + if importStart == nil { + return nil + } + + otherStart := otherRe.FindIndex(data) + if otherStart != nil && otherStart[0] < importStart[0] { + return nil + } + + groups := importRe.Groups(string(data)) + if groups.Index(0) == "" { + return nil + } + + imports := strings.Split(groups.Index(1), "\n") + for idx, i := range imports { + imports[idx] = strings.TrimSpace(i) + } + + formatted := formatImportStmt(splitImports(imports)) + if ctx.Bool("write") { + replaced := strings.Replace(string(data), groups.Index(0), formatted, 1) + return ioutil.WriteFile(walkPath, []byte(replaced), walkInfo.Mode()) + } + + if !strings.EqualFold(groups.Index(0), formatted) { + failed = true + beaver.Infof("File: %s", diffColor.Format(walkPath)) + beaver.Infof("Expected:\n%s", diffColor.Format(formatted)) + beaver.Infof("Got:\n%s\n", diffColor.Format(groups.Index(0))) + } + return nil + }); err != nil { + return err + } + if failed { + return errors.New("imports are formatted incorrectly; this can be fixed with the `--write` flag") + } + return nil +} + +func checkSkip(walkPath string, walkInfo os.FileInfo, excludePaths []string) walkStatus { + + // Skip current directory + if strings.EqualFold(walkPath, ".") { + return SKIP + } + + // Skip hidden paths (starting with ".") + if strings.HasPrefix(walkPath, ".") { + beaver.Debugf("Skipping hidden path %s", walkPath) + if walkInfo.IsDir() { + return SKIPDIR + } + return SKIP + } + + // Skip directories + if walkInfo.IsDir() { + return SKIP + } + + // Skip non-Go files + if !strings.HasSuffix(walkInfo.Name(), ".go") { + beaver.Debugf("Skipping non-Go file %s", walkPath) + return SKIP + } + + // Skip excluded directories + for _, e := range excludePaths { + if strings.HasPrefix(walkPath, e) { + beaver.Debugf("Skipping file %s because of exclude path %s", walkPath, e) + return SKIP + } + } + return CHECK +} + +func splitImports(imports []string) []importList { + // 0 -> stdlib + // 1 -> this module + // 2 -> others + split := []importList{{}, {}, {}} + var comment string + for _, imp := range imports { + if imp == "" { + continue + } + i := imp + if strings.HasPrefix(i, `//`) { + var nl string + if comment != "" { + nl = "\n" + } + comment = nl + i + continue + } + + var name string + if !strings.HasPrefix(i, `"`) { + parts := strings.Split(i, " ") + name = parts[0] + i = parts[1] + } + switch { + case strings.HasPrefix(i, `"`+module): + split[1] = append(split[1], importItem{comment, name, i}) + case strings.Contains(i, "."): + split[2] = append(split[2], importItem{comment, name, i}) + default: + split[0] = append(split[0], importItem{comment, name, i}) + } + comment = "" + } + + for _, s := range split { + sort.Sort(s) + } + return split +} + +func formatImportStmt(imports []importList) string { + var decl string + for _, imp := range imports { + var pre string + if decl != "" { + pre = "\n\n\t" + } + if len(imp) > 0 { + decl += pre + strings.Join(imp.StringSlice(), "\n\t") + } + } + return fmt.Sprintf(`import ( + %s +)`, decl) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..741ed97 --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "os" + + "github.com/urfave/cli/v2" + "go.jolheiser.com/beaver" +) + +func main() { + app := cli.NewApp() + app.Name = "Imp" + app.Usage = "Re-order imports" + app.Flags = []cli.Flag{ + &cli.BoolFlag{ + Name: "write", + Aliases: []string{"w"}, + Usage: "Write the re-ordered imports instead of just printing them", + }, + &cli.StringSliceFlag{ + Name: "exclude", + Aliases: []string{"e"}, + Usage: "A list of directories to exclude", + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"v"}, + Usage: "Print more information", + }, + } + app.Action = runImp + + if err := app.Run(os.Args); err != nil { + beaver.Fatal(err) + } +} diff --git a/struct.go b/struct.go new file mode 100644 index 0000000..b27b828 --- /dev/null +++ b/struct.go @@ -0,0 +1,49 @@ +package main + +type importItem struct { + Comment string + Name string + Path string +} + +func (ii importItem) String() string { + var comment string + if ii.Comment != "" { + comment = ii.Comment + "\n\t" + } + var name string + if ii.Name != "" { + name = ii.Name + " " + } + return comment + name + ii.Path +} + +type importList []importItem + +func (il importList) Len() int { + return len(il) +} + +func (il importList) Less(i, j int) bool { + return il[i].Path < il[j].Path +} + +func (il importList) Swap(i, j int) { + il[i], il[j] = il[j], il[i] +} + +func (il importList) StringSlice() []string { + s := make([]string, len(il)) + for idx, ii := range il { + s[idx] = ii.String() + } + return s +} + +type walkStatus int + +const ( + SKIP walkStatus = iota + SKIPDIR + CHECK +)