moby--moby/rm-gocheck.go

473 lines
12 KiB
Go

// +build ignore
package main
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"go/format"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
)
var (
shouldCommit = flag.Bool("commit", false, "if set, each step will result in a commit")
filter = flag.String("filter", "", "only run on files matching filter")
titlePrefix = flag.String("prefix", "rm-gocheck: ", "commit title prefix")
allFiles []string
fileToCmp = map[string]string{}
cmps = map[string][]string{}
)
type action func(*step) string
type step struct {
files []string
pkgs map[string]string
title string
pattern string
action action
comment string
}
func mustSh(format string, args ...interface{}) (output []string) {
var err error
output, err = sh(format, args...)
if err != nil {
panic(err)
}
return
}
func sh(format string, args ...interface{}) (output []string, err error) {
cmdargs := fmt.Sprintf(format, args...)
out, err := exec.Command("sh", "-c", cmdargs).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("cmd=%s\nout=%s\n", cmdargs, out)
}
l := strings.Split(string(out), "\n")
// remove last element if empty
if len(l[len(l)-1]) == 0 {
l = l[:len(l)-1]
}
return l, nil
}
func listToArgs(l []string) string {
s := fmt.Sprintf("%q", l)
s = s[1 : len(s)-1]
return s
}
func Replace(subst string) action {
return func(s *step) string {
return fmt.Sprintf("sed -E -i 's#%s#%s#g' \\\n-- %s", s.pattern, subst, listToArgs(s.files))
}
}
func CmpReplace(subst string) action {
return func(s *step) string {
var allCmdArgs, filesNeedingCmpImport []string
for _, file := range s.files {
cmp, ok := fileToCmp[file]
if !ok {
cmp = "cmp"
l := mustSh(`grep -m 1 -F '"gotest.tools/assert/cmp"' %s | awk '{print $1}'`, file)
if len(l) > 0 {
cmp = l[0]
} else {
filesNeedingCmpImport = append(filesNeedingCmpImport, file)
}
fileToCmp[file] = cmp
cmps[cmp] = append(cmps[cmp], file)
}
}
if len(filesNeedingCmpImport) > 0 {
linesep := " \\\n"
importCmd := fmt.Sprintf(`sed -E -i '0,/^import "github\.com/ s/^(import "github\.com.*)/\1\nimport "gotest.tools\/assert\/cmp")/'%s-- %s`, linesep, listToArgs(filesNeedingCmpImport))
allCmdArgs = append(allCmdArgs, importCmd)
importCmd = fmt.Sprintf(`sed -E -i '0,/^\t+"github\.com/ s/(^\t+"github\.com.*)/\1\n"gotest.tools\/assert\/cmp"/'%s-- %s`, linesep, listToArgs(filesNeedingCmpImport))
allCmdArgs = append(allCmdArgs, importCmd)
}
for cmp, files := range cmps {
cmdargs := fmt.Sprintf("sed -E -i 's#%s#%s#g' \\\n-- %s", s.pattern, strings.ReplaceAll(subst, "${cmp}", cmp), listToArgs(files))
allCmdArgs = append(allCmdArgs, cmdargs)
}
return strings.Join(allCmdArgs, " \\\n&& \\\n")
}
}
func redress(pattern string, files ...string) error {
rgx, err := regexp.Compile(pattern)
if err != nil {
return err
}
if len(files) == 0 {
return errors.New("no files provided")
}
fn := func(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
tmpName := file + ".tmp"
fixed, err := os.Create(tmpName)
if err != nil {
return err
}
defer fixed.Close()
const (
searching = iota
found
line_done
)
state := searching
s := bufio.NewScanner(f)
for s.Scan() {
b := s.Bytes()
if state != found {
bb := bytes.TrimRight(b, " \t")
if state == line_done && len(bb) == 0 {
continue
}
state = searching
if !rgx.Match(b) {
fixed.Write(b)
fixed.Write([]byte{'\n'})
} else {
fixed.Write(bb)
fixed.Write([]byte{' '})
state = found
}
continue
}
b = bytes.TrimRight(b, " \t")
fixed.Write(b)
if len(b) > 0 {
switch b[len(b)-1] {
case ',', '(':
fixed.Write([]byte{' '})
continue
case ')':
fixed.Write([]byte{'\n'})
state = line_done
}
}
}
if err := s.Err(); err != nil {
return err
}
fixed.Close()
f.Close()
src, err := ioutil.ReadFile(tmpName)
if err != nil {
return err
}
src, err = format.Source(src)
if err != nil {
return err
}
os.Remove(tmpName)
return ioutil.WriteFile(file, src, 0644)
}
var wg sync.WaitGroup
wg.Add(len(files))
for _, file := range files {
go func(file string) {
defer wg.Done()
if err := fn(file); err != nil {
panic(fmt.Sprintf("redress %s: %v", file, err))
}
}(file)
}
wg.Wait()
return nil
}
func Redress(s *step) string {
return fmt.Sprintf("go run rm-gocheck.go redress '%s' \\\n %s", s.pattern, listToArgs(s.files))
}
func Format(s *step) string {
pkgs := make([]string, 0, len(s.pkgs))
for dir := range s.pkgs {
pkgs = append(pkgs, "./"+dir)
}
files := listToArgs(pkgs)
return fmt.Sprintf("goimports -w \\\n-- %s \\\n&& \\\n gofmt -w -s \\\n-- %s", files, files)
}
func CommentInterface(s *step) string {
cmds := make([]string, 0, len(s.pkgs))
for dir := range s.pkgs {
cmd := fmt.Sprintf(`while :; do \
out=$(go test -c ./%s 2>&1 | grep 'cannot use nil as type string in return argument') || break
echo "$out" | while read line; do
file=$(echo "$line" | cut -d: -f1)
n=$(echo "$line" | cut -d: -f2)
sed -E -i "${n}"'s#\b(return .*, )nil#\1""#g' "$file"
done
done`, dir)
cmds = append(cmds, cmd)
}
return strings.Join(cmds, " \\\n&& \\\n")
}
func Eg(template string, prehook action, helperTypes string) action {
return func(s *step) string {
cmds := make([]string, 0, 3+4*len(s.pkgs))
if prehook != nil {
cmds = append(cmds, prehook(s))
}
cmdstr := fmt.Sprintf(`go get -d golang.org/x/tools/cmd/eg && dir=$(go env GOPATH)/src/golang.org/x/tools && git -C "$dir" fetch https://github.com/tiborvass/tools handle-variadic && git -C "$dir" checkout 61a94b82347c29b3289e83190aa3dda74d47abbb && go install golang.org/x/tools/cmd/eg`)
cmds = append(cmds, cmdstr)
for dir, pkg := range s.pkgs {
cmds = append(cmds, fmt.Sprintf(`/bin/echo -e 'package %s\n%s' > ./%s/eg_helper.go`, pkg, helperTypes, dir))
cmds = append(cmds, fmt.Sprintf(`goimports -w ./%s`, dir))
cmds = append(cmds, fmt.Sprintf(`eg -w -t %s -- ./%s`, template, dir))
cmds = append(cmds, fmt.Sprintf(`rm -f ./%s/eg_helper.go`, dir))
}
cmds = append(cmds, fmt.Sprintf("go run rm-gocheck.go redress '%s' \\\n %s", `\bassert\.Assert\b.*(\(|,)\s*$`, listToArgs(s.files)))
return strings.Join(cmds, " \\\n&& \\\n")
}
}
func do(steps []step) {
fileArgs := listToArgs(allFiles)
for _, s := range steps {
fmt.Print(s.title, "... ")
s.files, _ = sh(`git grep --name-only -E '%s' -- %s`, s.pattern, fileArgs)
if len(s.files) == 0 {
fmt.Println("no files match")
continue
}
s.pkgs = map[string]string{}
pkg := ""
if len(s.files) > 0 {
x := mustSh(`grep -m1 '^package ' -- %s | cut -d' ' -f2`, s.files[0])
pkg = x[0]
}
for _, file := range s.files {
s.pkgs[filepath.Dir(file)] = pkg
}
cmdstr := s.action(&s)
mustSh(cmdstr)
if *shouldCommit {
if len(s.comment) > 0 {
s.comment = "\n\n" + s.comment
}
msg := fmt.Sprintf("%s%s\n\n%s%s", *titlePrefix, s.title, cmdstr, s.comment)
sh(`git add %s`, listToArgs(s.files))
cmd := exec.Command("git", "commit", "-s", "-F-")
cmd.Stdin = strings.NewReader(msg)
out, err := cmd.CombinedOutput()
if err != nil {
panic(string(out))
}
fmt.Println("committed")
} else {
fmt.Println("done")
}
}
}
func main() {
flag.Parse()
args := flag.Args()
if len(args) > 0 {
switch cmd := args[0]; cmd {
case "redress":
if len(args) < 3 {
panic(fmt.Sprintf("usage: %s [flags] redress <pattern> <files...>", os.Args[0]))
}
if err := redress(args[1], args[2:]...); err != nil {
panic(fmt.Sprintf("redress: %v", err))
}
return
default:
panic(fmt.Sprintf("unknown command %s", cmd))
}
}
allFiles, _ = sh(`git grep --name-only '"github.com/go-check/check"' :**.go | grep -vE '^(vendor/|integration-cli/checker|rm-gocheck\.go|template\..*\.go)' | grep -E '%s'`, *filter)
if len(allFiles) == 0 {
return
}
do([]step{
{
title: "normalize c.Check to c.Assert",
pattern: `\bc\.Check\(`,
action: Replace(`c.Assert(`),
},
{
title: "redress multiline c.Assert calls",
pattern: `\bc\.Assert\b.*(,|\()\s*$`,
action: Redress,
},
{
title: "c.Assert(...) -> assert.Assert(c, ...)",
pattern: `\bc\.Assert\(`,
action: Replace(`assert.Assert(c, `),
},
{
title: "check.C -> testing.B for BenchmarkXXX",
pattern: `( Benchmark[^\(]+\([^ ]+ \*)check\.C\b`,
action: Replace(`\1testing.B`),
},
{
title: "check.C -> testing.T",
pattern: `\bcheck\.C\b`,
action: Replace(`testing.T`),
},
{
title: "ErrorMatches -> assert.ErrorContains",
pattern: `\bassert\.Assert\(c, (.*), check\.ErrorMatches,`,
action: Replace(`assert.ErrorContains(c, \1,`),
},
{
title: "normalize to use checker",
pattern: `\bcheck\.(Equals|DeepEquals|HasLen|IsNil|Matches|Not|NotNil)\b`,
action: Replace(`checker.\1`),
},
{
title: "Not(IsNil) -> != nil",
pattern: `\bassert\.Assert\(c, (.*), checker\.Not\(checker\.IsNil\)`,
action: Replace(`assert.Assert(c, \1 != nil`),
},
{
title: "Not(Equals) -> a != b",
pattern: `\bassert\.Assert\(c, (.*), checker\.Not\(checker\.Equals\), (.*)`,
action: Replace(`assert.Assert(c, \1 != \2`),
},
{
title: "Not(Matches) -> !cmp.Regexp",
pattern: `\bassert\.Assert\(c, (.*), checker\.Not\(checker\.Matches\), (.*)\)`,
action: CmpReplace(`assert.Assert(c, !${cmp}.Regexp("^"+\2+"$", \1)().Success())`),
},
{
title: "Equals -> assert.Equal",
pattern: `\bassert\.Assert\(c, (.*), checker\.Equals, (.*)`,
action: Replace(`assert.Equal(c, \1, \2`),
},
{
title: "DeepEquals -> assert.DeepEqual",
pattern: `\bassert\.Assert\(c, (.*), checker\.DeepEquals, (.*)`,
action: Replace(`assert.DeepEqual(c, \1, \2`),
},
{
title: "HasLen -> assert.Equal + len()",
pattern: `\bassert\.Assert\(c, (.*), checker\.HasLen, (.*)`,
action: Replace(`assert.Equal(c, len(\1), \2`),
},
{
title: "IsNil",
pattern: `\bassert\.Assert\(c, (.*), checker\.IsNil\b`,
action: Replace(`assert.Assert(c, \1 == nil`),
},
{
title: "NotNil",
pattern: `\bassert\.Assert\(c, (.*), checker\.NotNil\b`,
action: Replace(`assert.Assert(c, \1 != nil`),
},
{
title: "False",
pattern: `\bassert\.Assert\(c, (.*), checker\.False\b`,
action: Replace(`assert.Assert(c, !\1`),
},
{
title: "True",
pattern: `\bassert\.Assert\(c, (.*), checker\.True`,
action: Replace(`assert.Assert(c, \1`),
},
{
title: "redress check.Suite calls",
pattern: `[^/]\bcheck\.Suite\(.*\{\s*$`,
action: Redress,
},
{
title: "comment out check.Suite calls",
pattern: `^([^*])+?((var .*)?check\.Suite\(.*\))`,
action: Replace(`\1/*\2*/`),
},
{
title: "comment out check.TestingT",
pattern: `([^*])(check\.TestingT\([^\)]+\))`,
action: Replace(`\1/*\2*/`),
},
{
title: "run goimports to compile successfully",
action: Format,
},
{
title: "Matches -> cmp.Regexp",
pattern: `\bassert\.Assert\(c, (.*), checker\.Matches, (.*)\)$`,
action: Eg("template.matches.go",
CmpReplace(`assert.Assert(c, eg_matches(${cmp}.Regexp, \1, \2))`),
`var eg_matches func(func(cmp.RegexOrPattern, string) cmp.Comparison, interface{}, string, ...interface{}) bool`),
},
{
title: "Not(Contains) -> !strings.Contains",
pattern: `\bassert\.Assert\(c, (.*), checker\.Not\(checker\.Contains\), (.*)\)$`,
action: Eg("template.not_contains.go",
Replace(`assert.Assert(c, !eg_contains(\1, \2))`),
`var eg_contains func(arg1, arg2 string, extra ...interface{}) bool`),
},
{
title: "Contains -> strings.Contains",
pattern: `\bassert\.Assert\(c, (.*), checker\.Contains, (.*)\)$`,
action: Eg("template.contains.go",
Replace(`assert.Assert(c, eg_contains(\1, \2))`),
`var eg_contains func(arg1, arg2 string, extra ...interface{}) bool`),
},
{
title: "convert check.Commentf to string - with multiple args",
pattern: `\bcheck.Commentf\(([^,]+),(.*)\)`,
action: Replace(`fmt.Sprintf(\1,\2)`),
},
{
title: "convert check.Commentf to string - with just one string",
pattern: `\bcheck.Commentf\(("[^"]+")\)`,
action: Replace(`\1`),
},
{
title: "convert check.Commentf to string - other",
pattern: `\bcheck.Commentf\(([^\)]+)\)`,
action: Replace(`\1`),
},
{
title: "check.CommentInterface -> string",
pattern: `(\*testing\.T\b.*)check\.CommentInterface\b`,
action: Replace(`\1string`),
},
{
title: "goimports",
action: Format,
},
{
title: "fix compile errors from converting check.CommentInterface to string",
action: CommentInterface,
},
})
}