[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)
+			}
+		})
+	}
+}