[gotidy] Add a tool for tidying Go source code
This should only be used to perform refactors that are
"mandatory", not matters of personal preference. e.g.
* Running gofmt
* Adding Fuchsia license headers at the tops of files.
Change-Id: I87ae2bbe5aff2829f038870fd17a844d50baf6fc
diff --git a/cmd/gotidy/main.go b/cmd/gotidy/main.go
new file mode 100644
index 0000000..8dc2992
--- /dev/null
+++ b/cmd/gotidy/main.go
@@ -0,0 +1,176 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "go/format"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "fuchsia.googlesource.com/infra/infra/gotidy"
+)
+
+// TidyStep transforms source code into other source code.
+type TidyStep interface {
+ Tidy([]byte) ([]byte, error)
+}
+
+// TidyFunc is an adapter for functions with the same signature as TidyStep.Tidy.
+type TidyFunc func([]byte) ([]byte, error)
+
+// Tidy implements TidyStep.
+func (fn TidyFunc) Tidy(input []byte) ([]byte, error) {
+ return fn(input)
+}
+
+var (
+ // Whether to overwrite the input file contents with tidied output.
+ overwrite bool
+
+ // Whether to display usage and exit.
+ help bool
+
+ // The list of tidy steps to run.
+ tidySteps = []TidyStep{
+ &gotidy.AddCopyrightHeader{Year: time.Now().Year()},
+ TidyFunc(format.Source), // gofmt. Always do this last.
+ }
+)
+
+func usage() {
+ fmt.Fprint(os.Stderr, "gotidy [flags] [path...]")
+ flag.PrintDefaults()
+}
+
+func init() {
+ flag.BoolVar(&help, "h", false, "Display usage and exit")
+ flag.BoolVar(&overwrite, "w", false, "Whether to overwrite files with tidied output")
+ flag.Usage = usage
+}
+
+func main() {
+ flag.Parse()
+
+ if help || flag.NArg() == 0 {
+ flag.Usage()
+ return
+ }
+
+ os.Exit(execute(flag.Args()))
+}
+
+func execute(paths []string) int {
+ if len(paths) == 0 {
+ paths = []string{"."}
+ }
+
+ var files []string
+ for _, path := range paths {
+ files = append(files, listGoFilesRecursive(path)...)
+ }
+
+ return exitCode(tidyFiles(files))
+}
+
+func listGoFilesRecursive(root string) []string {
+ var paths []string
+ err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() && path != root {
+ paths = append(paths, listGoFilesRecursive(path)...)
+ }
+
+ if strings.HasSuffix(path, ".go") {
+ paths = append(paths, path)
+ }
+ return nil
+ })
+
+ if err != nil {
+ return []string{}
+ }
+
+ return paths
+}
+
+func tidyFiles(files []string) <-chan error {
+ errs := make(chan error)
+
+ var wg sync.WaitGroup
+ for _, file := range files {
+ wg.Add(1)
+ log.Println("tidying", file)
+ go tidyFile(file, errs, &wg)
+ }
+
+ go func() {
+ wg.Wait()
+ close(errs)
+ }()
+
+ return errs
+}
+
+func tidyFile(file string, errs chan<- error, wg *sync.WaitGroup) {
+ input, err := ioutil.ReadFile(file)
+ if err != nil {
+ errs <- fmt.Errorf("failed to read %s: %v", file, err)
+ }
+
+ tidied, err := tidy(input)
+ if err != nil {
+ errs <- fmt.Errorf("failed to tidy %s: %v", file, err)
+ }
+
+ reader := bytes.NewReader(tidied)
+ dest := os.Stdout
+ if overwrite {
+ dest, err = os.Create(file)
+ if err != nil {
+ errs <- fmt.Errorf("failed to open %s: %v", file, err)
+ }
+ }
+
+ if _, err := io.Copy(dest, reader); err != nil {
+ errs <- fmt.Errorf("failed to write output to %s: %v", file, err)
+ }
+
+ wg.Done()
+}
+
+func tidy(input []byte) ([]byte, error) {
+ source := input
+
+ for _, tidyStep := range tidySteps {
+ output, err := tidyStep.Tidy(source)
+ if err != nil {
+ return nil, err
+ }
+ source = output
+ }
+
+ return source, nil
+}
+
+func exitCode(in <-chan error) int {
+ var code int
+ for err := range in {
+ log.Println(err)
+ code = 1
+ }
+ return code
+}
diff --git a/go.sum b/go.sum
index 36c5aac..7cf1b88 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,9 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.29.0 h1:gv/9Wwq5WPVIGaROMQg8tw4jLFFiyacODxEIrlz0wTw=
cloud.google.com/go v0.29.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/docker/distribution v0.0.0-20171207180435-f4118485915a h1:5t1N6zH0izFVQQaxYsR+1l4pSRo1tNqFbUcfEbYI0a8=
github.com/docker/distribution v0.0.0-20171207180435-f4118485915a/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -13,7 +15,9 @@
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -33,6 +37,7 @@
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/julienschmidt/httprouter v1.1.0 h1:7wLdtIiIpzOkC9u6sXOozpBauPdskj3ru4EI5MABq68=
github.com/julienschmidt/httprouter v1.1.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/maruel/subcommands v0.0.0-20161130014849-8c2c452e1460 h1:0r85vsW8QTxF9nD96S0ryuip/sA56FdUyFNeaD/H+Q4=
github.com/maruel/subcommands v0.0.0-20161130014849-8c2c452e1460/go.mod h1:4cd1CVd4c9phb1z9fTkV+JbmnFm394Hp9rHEAOvD+vs=
@@ -52,6 +57,7 @@
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3 h1:35ZwriXqdZtBGoFgUpW71Z7xz5o23fRpWHFAO2PlnIA=
golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7 h1:00BeQWmeaGazuOrq8Q5K5d3/cHaGuFrZzpaHBXfrsUA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824 h1:MkjFNbaZJyH98M67Q3umtwZ+EdVdrNJLqSwZp5vcv60=
@@ -59,20 +65,24 @@
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced h1:4oqSq7eft7MdPKBGQK11X9WYUxmj6ZLgGTqYIbY1kyw=
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181004145325-8469e314837c h1:SJ7JoQNVl3mC7EWkkONgBWgCno8LcABIJwFMkWBC+EY=
golang.org/x/sys v0.0.0-20181004145325-8469e314837c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/api v0.0.0-20181003000758-f5c49d98d21c h1:qSBE8MLMBtzNDa9QWZiS0qSIAYpU4BbVXbM70aNG55g=
google.golang.org/api v0.0.0-20181003000758-f5c49d98d21c/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/appengine v0.0.0-20170814190942-d9a072cfa7b9/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f h1:FU37niK8AQ59mHcskRyQL7H0ErSeNh650vdcj8HqdSI=
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw=
google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858 h1:wN+eVZ7U+gqdqkec6C6VXR1OFf9a5Ul9ETzeYsYv20g=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/gotidy/add_copyright_header.go b/gotidy/add_copyright_header.go
new file mode 100644
index 0000000..1055dcd
--- /dev/null
+++ b/gotidy/add_copyright_header.go
@@ -0,0 +1,58 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package gotidy
+
+import (
+ "fmt"
+ "go/scanner"
+ "go/token"
+ "strings"
+)
+
+var (
+ copyrightTemplate = strings.TrimSpace(`
+// Copyright %d The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.`)
+)
+
+// AddCopyrightHeader inserts the Fuchsia copyright header into a file if no copyright
+// header is present.
+type AddCopyrightHeader struct {
+ Year int
+}
+
+func (t *AddCopyrightHeader) Tidy(input []byte) ([]byte, error) {
+ var s scanner.Scanner
+ fset := token.NewFileSet()
+ file := fset.AddFile("", fset.Base(), len(input)) // register file
+ s.Init(file, input, nil /* no error handler */, scanner.ScanComments)
+
+ for {
+ pos, tok, literal := s.Scan()
+ // We scanned the entire file without finding the header.
+ if tok == token.EOF {
+ return t.addCopyrightHeader(input)
+ }
+
+ // We moved past the start of the file. It's missing the header.
+ if tok == token.COMMENT && pos > 1 {
+ return t.addCopyrightHeader(input)
+ }
+
+ if tok == token.COMMENT {
+ // The file starts with a comment and it has the header
+ if strings.HasPrefix(strings.ToLower(literal), "// copyright") {
+ return input, nil
+ }
+ return t.addCopyrightHeader(input)
+ }
+ }
+}
+
+func (t *AddCopyrightHeader) addCopyrightHeader(input []byte) ([]byte, error) {
+ header := fmt.Sprintf(copyrightTemplate, t.Year)
+ return []byte(header + "\n\n" + string(input)), nil
+}
diff --git a/gotidy/add_copyright_header_test.go b/gotidy/add_copyright_header_test.go
new file mode 100644
index 0000000..dfda815
--- /dev/null
+++ b/gotidy/add_copyright_header_test.go
@@ -0,0 +1,149 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package gotidy_test
+
+import (
+ "go/format"
+ "testing"
+
+ "fuchsia.googlesource.com/infra/infra/gotidy"
+)
+
+func gofmt(t *testing.T, input string) string {
+ output, err := format.Source([]byte(input))
+ if err != nil {
+ t.Errorf("maformed input: %v", err)
+ }
+ return string(output)
+}
+
+func TestCopyrightHeader(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ step *gotidy.AddCopyrightHeader
+ expected string
+ }{
+ {
+ name: "should handle unformatted input",
+ step: &gotidy.AddCopyrightHeader{Year: 2019},
+ input: `package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello, World!")
+}
+`,
+ expected: `// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello, World!")
+}
+`},
+ {
+ name: "should add copyright if missing",
+ step: &gotidy.AddCopyrightHeader{Year: 2019},
+ input: gofmt(t, `
+ package main
+
+ import "fmt"
+
+ func main() {
+ fmt.Println("Hello, World!")
+ }
+ `),
+ expected: gofmt(t, `
+ // Copyright 2019 The Fuchsia Authors. All rights reserved.
+ // Use of this source code is governed by a BSD-style license that can be
+ // found in the LICENSE file.
+
+ package main
+
+ import "fmt"
+
+ func main() {
+ fmt.Println("Hello, World!")
+ }
+ `),
+ }, {
+ name: "should not overwrite existing header",
+ step: &gotidy.AddCopyrightHeader{Year: 2019},
+ input: gofmt(t, `
+ // Copyright 2015 The Fuchsia Authors. All rights reserved.
+ // Use of this source code is governed by a BSD-style license that can be
+ // found in the LICENSE file.
+
+ package main
+
+ import "fmt"
+
+ func main() {
+ fmt.Println("Hello, World!")
+ }
+ `),
+ expected: gofmt(t, `
+ // Copyright 2015 The Fuchsia Authors. All rights reserved.
+ // Use of this source code is governed by a BSD-style license that can be
+ // found in the LICENSE file.
+
+ package main
+
+ import "fmt"
+
+ func main() {
+ fmt.Println("Hello, World!")
+ }
+ `),
+ }, {
+ name: "should preserve package doc comment",
+ step: &gotidy.AddCopyrightHeader{Year: 2019},
+ // No space between package decl and header
+ input: gofmt(t, `
+ // Package main says hello.
+ package main
+
+ import "fmt"
+
+ func main() {
+ fmt.Println("Hello, World!")
+ }
+ `),
+ expected: gofmt(t, `
+ // Copyright 2019 The Fuchsia Authors. All rights reserved.
+ // Use of this source code is governed by a BSD-style license that can be
+ // found in the LICENSE file.
+
+ // Package main says hello.
+ package main
+
+ import "fmt"
+
+ func main() {
+ fmt.Println("Hello, World!")
+ }
+ `),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ output, err := tt.step.Tidy([]byte(tt.input))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(output) != tt.expected {
+ t.Errorf("case '%s'\ngot:\n'%s'\nwant:\n'%s'\n", tt.name, output, tt.expected)
+ }
+ })
+ }
+}