diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json new file mode 100644 index 0000000..5671b64 --- /dev/null +++ b/.vscode/cSpell.json @@ -0,0 +1,29 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.1 + "version": "0.1", + // language - current active spelling language + "language": "en", + // words - list of words to be always considered correct + "words": [ + "Iface", + "astrewrite", + "cmap", + "geireplacer", + "genny", + "genx", + "irepl", + "lmap", + "sflags", + "tions", + "tmpl", + "vals", + "xsel" + ], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + // For example "hte" should be "the" + "flagWords": [ + "hte" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..251d67b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.enabled": true +} diff --git a/README.md b/README.md index 33aca11..9cfefbf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ * If you intend on generating files in the same package, you may add `// +build genx` to your template(s). * Transparently handles [genny](https://github.com/cheekybits/genny)'s `generic.Type`. * Supports a few [seeds](https://github.com/OneOfOne/genx/tree/master/seeds/). -* Adds build tags based on the types you pass, so you can target specifc types (ex: `// +build genx_t_string` or `// +build genx_vt_builtin` ) +* Adds build tags based on the types you pass, so you can target specific types (ex: `// +build genx_t_string` or `// +build genx_vt_builtin` ) * Automatically handles nil returns, will return the zero value of the type. * Doesn't need modifying the source package if there's only one type involved. @@ -52,7 +52,7 @@ ➤ genx -f github.com/OneOfOne/cmap/lmap.go -t "KT=string,VT=int" -fn "NewLMap,NewLMapSize=NewStringInt" -n main -v -o ./lmap_string_int.go ``` -### Modifing an external library that doesn't specifically support generics: +### Modifying an external library that doesn't specifically support generics: Using [fatih](https://github.com/fatih)'s excellent [set](https://github.com/fatih/set) library: ``` @@ -150,7 +150,7 @@ For Example I needed to remove a field from the struct and change all usage of i * Handle removing comments properly rather than using regexp. * More seeds. * ~~Add proper examples.~~ -* ~~Support specalized functions by type.~~ +* ~~Support specialized functions by type.~~ * ~~Support removing structs and their methods.~~ ## Credits @@ -160,54 +160,38 @@ For Example I needed to remove a field from the struct and change all usage of i ## Usage ([`cmd/genx`](https://github.com/OneOfOne/genx/tree/master/cmd/genx/main.go)): ``` -usage: genx [-t T=type] [-s xx.xx=[yy.]yy] [-fld struct-field-to-remove] [-fn func-to-remove] [-tags "go build tags"] - [-m] [-n package-name] [-pkg input package] [-f input file] [-o output file or dir] - -Types: - The -t/-s flags supports full package paths or short ones and letting goimports handle it. - -t "KV=string - -t "M=*cmap.CMap" - -t "M=github.com/OneOfOne/cmap.*CMap" - -s "cm.HashFn=github.com/OneOfOne/cmap/hashers#H.Fnv32" - -s "cm.HashFn=github.com/OneOfOne/cmap/hashers.Fnv32" - -s "cm.HashFn=hashers.Fnv32" - -t "RemoveThisType" - -fld "RemoveThisStructField,OtherField=NewFieldName" - -fn "RemoveThisFunc,OtherFunc=NewFuncName" - -Examples: - genx -pkg github.com/OneOfOne/cmap -t "KT=interface{},VT=interface{}" -m -n cmap -o ./cmap.go - genx -f github.com/OneOfOne/cmap/lmap.go -t "KT=string,VT=int" -fn "NewLMap,NewLMapSize=NewStringInt" -n main -o ./lmap_string_int.go - - genx -pkg github.com/OneOfOne/cmap -n stringcmap -t KT=string -t VT=interface{} -fld HashFn \ - -fn DefaultKeyHasher -s "cm.HashFn=hashers.Fnv32" -m -o ./stringcmap/cmap.go - -Flags: - -f file - file to parse - -fld field - struct fields to remove or rename (ex: -fld HashFn -fld priv=Pub) - -fn value - func`s to remove or rename (ex: -fn NotNeededFunc -fn Something=SomethingElse) - -get - go get the package if it doesn't exist - -goFlags flags - extra go get flags (ex: -goFlags '-t -race') - -n package name - package name sets the output package name, uses input package name if empty. - -o string - output dir if parsing a package or output filename if parsing a file (default "/dev/stdin") - -pkg package - package to parse - -s selector spec - selector specs to remove or rename (ex: -s 'cm.HashFn=hashers.Fnv32' -s 'x.Call=Something') - -seed - alias for -m -pkg github.com/OneOfOne/seeds/ - -t type spec - generic type specs to remove or rename (ex: -t 'KV=string,KV=interface{}' -t RemoveThisType) - -tags tags - go build tags, used for parsing and automatically passed to go get. - -v verbose +➤ genx -h +NAME: + genx - Generics For Go, Yet Again. + +USAGE: + genx [global options] command [command options] [arguments...] + +VERSION: + v0.5 + +AUTHOR: + Ahmed W. gmail com> + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --seed seed-name alias for -pkg github.com/OneOfOne/genx/seeds/seed-name + --in file, -f file file to process, use `-` to process stdin. + --package package, --pkg package package to process. + --name name, -n name package name to use for output, uses the input package's name by default. + --type type, -t type generic type names to remove or rename (ex: -t 'KV=string,KV=interface{}' -t RemoveThisType). + --selector selector, -s selector selectors to remove or rename (ex: -s 'cm.HashFn=hashers.Fnv32' -s 'x.Call=Something'). + --field field, --fld field struct fields to remove or rename (ex: -fld HashFn -fld privateFunc=PublicFunc). + --func func, --fn func functions to remove or rename (ex: -fn NotNeededFunc -fn Something=SomethingElse). + --out value, -o value output dir if parsing a package or output filename if you want the output to be merged. (default: "/dev/stdout") + --tags value go extra build tags, used for parsing and automatically passed to any go subcommands. + --goFlags flags extra flags to pass to go subcommands flags (ex: --goFlags '-race') + --get go get the package if it doesn't exist (default: false) + --verbose, -v verbose output (default: false) + --help, -h show help (default: false) + --version, -V print the version (default: false) ``` ## Contributions diff --git a/all_types.go b/all_types.go index f6d4107..566037e 100644 --- a/all_types.go +++ b/all_types.go @@ -4,45 +4,66 @@ package genx import "github.com/cheekybits/genny/generic" -type KT generic.Type -type VT interface{} +type T generic.Type -type BothKT struct { +type ( + KT interface{} + VT interface{} +) +type TypeWithKT struct { K KT V VT - Call func(k KT) VT - RemoveMeToo int // comment + Call func(k KT) VT + RemoveMe VT // RemoveMe comment + + Iface interface{} } -// RemoveMe comment -func (b *BothKT) RemoveMe() { +// MethodWithPtr comment +func (b *TypeWithKT) MethodWithPtr() { b.K = new(KT) b.V = new(VT) } -// some comment -func (b BothKT) RemoveMe2() { +// MethodWithValue comment +func (b TypeWithKT) MethodWithValue() { b.K = new(KT) b.V = new(VT) } +func DoParam(b *TypeWithKT) {} +func DoRes() *TypeWithKT { return nil } +func DoBoth(b *TypeWithKT) *TypeWithKT { return nil } + func DoStuff(k ...KT) VT { - var b BothKT + b := &TypeWithKT{ + RemoveMe: nil, + } return b.Call(k[0]) } +func DoStuffTwo(k ...KT) VT { + var b TypeWithKT + return b.RemoveMe +} + +func ReturnVT() VT { + return nil +} + var ( m map[KT]VT ktCh chan KT vtCh chan VT - kvCh chan Both + kvCh chan TypeWithKT ktA [100]KT vtA [100]VT - a [100]Both + a [100]TypeWithKT ktS []KT vtS []VT - s []Both + s []*TypeWithKT + mp map[*TypeWithKT]interface{} ) func XXX(vs ...interface{}) interface{} { diff --git a/cmd/genx/main.go b/cmd/genx/main.go index 32bb68c..533dc6e 100644 --- a/cmd/genx/main.go +++ b/cmd/genx/main.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "log" "os" @@ -9,97 +8,131 @@ import ( "path/filepath" "strings" + "github.com/OneOfOne/cli" "github.com/OneOfOne/genx" ) -type sflags []string - -func (sf *sflags) String() string { - return strings.Join(*sf, " ") -} - -func (sf *sflags) Set(value string) error { - parts := strings.Split(value, ",") - for i, p := range parts { - parts[i] = strings.TrimSpace(p) +type sflags []*[2]string + +func flattenFlags(in []string) (out sflags) { + for _, f := range in { + for _, p := range strings.Split(f, ",") { + kv := strings.Split(p, "=") + switch len(kv) { + case 2: + out = append(out, &[2]string{strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])}) + case 1: + out = append(out, &[2]string{strings.TrimSpace(kv[0])}) + } + } } - *sf = append(*sf, parts...) - return nil + return } -func (sf *sflags) Split(i int) (_, _ string) { - parts := strings.Split((*sf)[i], "=") - switch len(parts) { - case 2: - return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) - case 1: - return strings.TrimSpace(parts[0]), "" - default: - return +func main() { + log.SetFlags(log.Lshortfile) + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", + Aliases: []string{"V"}, + Usage: "print the version", } -} -var ( - types, fields, selectors, funcs, tags sflags - - seed, inFile, inPkg, outPath string - - goFlags string - pkgName string - mergeFiles bool - goGet bool - verbose bool -) + app := &cli.App{ + Name: "genx", + Usage: "Generics For Go, Yet Again.", + Version: "v0.5", + Authors: []*cli.Author{{ + Name: "Ahmed W.", + Email: "oneofone+genx gmail com", + }, + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "seed", + Usage: "alias for -pkg github.com/OneOfOne/genx/seeds/`seed-name`", + }, + &cli.StringFlag{ + Name: "in", + Aliases: []string{"f"}, + Usage: "`file` to process, use `-` to process stdin.", + }, + + &cli.StringFlag{ + Name: "package", + Aliases: []string{"pkg"}, + Usage: "`package` to process.", + }, + + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "package `name` to use for output, uses the input package's name by default.", + }, + + &cli.StringSliceFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "generic `type` names to remove or rename (ex: -t 'KV=string,KV=interface{}' -t RemoveThisType).", + }, + + &cli.StringSliceFlag{ + Name: "selector", + Aliases: []string{"s"}, + Usage: "`selector`s to remove or rename (ex: -s 'cm.HashFn=hashers.Fnv32' -s 'x.Call=Something').", + }, + + &cli.StringSliceFlag{ + Name: "field", + Aliases: []string{"fld"}, + Usage: "struct `field`s to remove or rename (ex: -fld HashFn -fld privateFunc=PublicFunc).", + }, + + &cli.StringSliceFlag{ + Name: "func", + Aliases: []string{"fn"}, + Usage: "`func`tions to remove or rename (ex: -fn NotNeededFunc -fn Something=SomethingElse).", + }, + + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Value: "/dev/stdout", + Usage: "output dir if parsing a package or output filename if you want the output to be merged.", + }, + + &cli.StringSliceFlag{ + Name: "tags", + Usage: "go extra build tags, used for parsing and automatically passed to any go subcommands.", + }, + + &cli.StringSliceFlag{ + Name: "goFlags", + Usage: "extra flags to pass to go subcommands `flags` (ex: --goFlags '-race')", + }, + + &cli.BoolFlag{ + Name: "get", + Usage: "go get the package if it doesn't exist", + }, + + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"v"}, + Usage: "verbose output", + }, + }, + Action: runGen, + } -func init() { - log.SetFlags(log.Lshortfile) - flag.Usage = func() { - fmt.Fprintln(os.Stderr, `usage: genx [-t T=type] [-s xx.xx=[yy.]yy] [-fld struct-field-to-remove] [-fn func-to-remove] [-tags "go build tags"] - [-m] [-n package-name] [-pkg input package] [-f input file] [-o output file or dir] - -Types: - The -t/-s flags supports full package paths or short ones and letting goimports handle it. - -t "KV=string - -t "M=*cmap.CMap" - -t "M=github.com/OneOfOne/cmap.*CMap" - -s "cm.HashFn=github.com/OneOfOne/cmap/hashers#H.Fnv32" - -s "cm.HashFn=github.com/OneOfOne/cmap/hashers.Fnv32" - -s "cm.HashFn=hashers.Fnv32" - -t "RemoveThisType" - -fld "RemoveThisStructField,OtherField=NewFieldName" - -fn "RemoveThisFunc,OtherFunc=NewFuncName" - -Examples: - genx -pkg github.com/OneOfOne/cmap -t "KT=interface{},VT=interface{}" -m -n cmap -o ./cmap.go - genx -f github.com/OneOfOne/cmap/lmap.go -t "KT=string,VT=int" -fn "NewLMap,NewLMapSize=NewStringInt" -n main -o ./lmap_string_int.go - - genx -pkg github.com/OneOfOne/cmap -n stringcmap -t KT=string -t VT=interface{} -fld HashFn \ - -fn DefaultKeyHasher -s "cm.HashFn=hashers.Fnv32" -m -o ./stringcmap/cmap.go - -Flags:`) - flag.PrintDefaults() - } - flag.Var(&types, "t", "generic `type spec`s to remove or rename (ex: -t 'KV=string,KV=interface{}' -t RemoveThisType)") - flag.Var(&selectors, "s", "`selector spec`s to remove or rename (ex: -s 'cm.HashFn=hashers.Fnv32' -s 'x.Call=Something')") - flag.Var(&fields, "fld", "struct `field`s to remove or rename (ex: -fld HashFn -fld priv=Pub)") - flag.Var(&funcs, "fn", "func`s to remove or rename (ex: -fn NotNeededFunc -fn Something=SomethingElse)") - flag.Var(&tags, "tags", "go build `tags`, used for parsing and automatically passed to go get.") - flag.StringVar(&seed, "seed", "", "alias for -m -pkg github.com/OneOfOne/seeds/``") - flag.StringVar(&inFile, "f", "", "`file` to parse") - flag.StringVar(&inPkg, "pkg", "", "`package` to parse") - flag.StringVar(&outPath, "o", "/dev/stdout", "output dir if parsing a package or output filename if parsing a file") - flag.StringVar(&pkgName, "n", "", "`package name` sets the output package name, uses input package name if empty.") - flag.StringVar(&goFlags, "goFlags", "", "extra go get `flags` (ex: -goFlags '-t -race')") - flag.BoolVar(&verbose, "v", false, "verbose") - flag.BoolVar(&goGet, "get", false, "go get the package if it doesn't exist") - - flag.Parse() + // TODO: support other actions + app.Run(os.Args) } -func main() { +func runGen(c *cli.Context) error { rewriters := map[string]string{} - for i := range types { - key, val := types.Split(i) + + for _, kv := range flattenFlags(c.StringSlice("type")) { + key, val := kv[0], kv[1] if key == "" { continue } @@ -108,8 +141,9 @@ func main() { } rewriters["type:"+key] = val } - for i := range selectors { - key, val := selectors.Split(i) + + for _, kv := range flattenFlags(c.StringSlice("selector")) { + key, val := kv[0], kv[1] if key == "" { continue } @@ -118,8 +152,10 @@ func main() { } rewriters["selector:"+key] = val } - for i := range fields { - key, val := fields.Split(i) + + for _, kv := range flattenFlags(c.StringSlice("field")) { + key, val := kv[0], kv[1] + if key == "" { continue } @@ -128,8 +164,10 @@ func main() { } rewriters["field:"+key] = val } - for i := range funcs { - key, val := funcs.Split(i) + + for _, kv := range flattenFlags(c.StringSlice("func")) { + key, val := kv[0], kv[1] + if key == "" { continue } @@ -139,14 +177,21 @@ func main() { rewriters["func:"+key] = val } - g := genx.New(pkgName, rewriters) - g.BuildTags = append(g.BuildTags, tags...) + g := genx.New(c.String("name"), rewriters) + g.BuildTags = append(g.BuildTags, c.StringSlice("tags")...) - if verbose { + if c.Bool("verbose") { log.Printf("rewriters: %+q", g.OrderedRewriters()) log.Printf("build tags: %+q", g.BuildTags) } + var ( + outPath = c.String("out") + + mergeFiles bool + inPkg string + ) + switch outPath { case "", "-", "/dev/stdout": outPath = "/dev/stdout" @@ -156,63 +201,69 @@ func main() { // auto merge files if the output is a file not a dir. mergeFiles = !mergeFiles && filepath.Ext(outPath) == ".go" - if seed != "" { + if seed := c.String("seed"); seed != "" { inPkg = "github.com/OneOfOne/genx/seeds/" + seed mergeFiles = true + } else { + inPkg = c.String("package") } if inPkg != "" { - out, err := goListThenGet(g.BuildTags, inPkg) + out, err := goListThenGet(c, g.BuildTags, inPkg) if err != nil { - log.Fatalf("error: %s\n", out) + return cli.Exit(err, 2) } + inPkg = out - // if !strings.HasPrefix(inpk, prefix string) pkg, err := g.ParsePkg(inPkg, false) + if err != nil { - log.Fatalf("error parsing package (%s): %v\n", inPkg, err) + return cli.Exit(fmt.Sprintf("error parsing package (%s): %v\n", inPkg, err), 1) } if mergeFiles { - if err := pkg.WriteAllMerged(outPath, false); err != nil { - log.Fatalf("error writing merged package: %v", err) - } + err = pkg.WriteAllMerged(outPath, false) } else { - if err := pkg.WritePkg(outPath); err != nil { - log.Fatalf("error writing merged package: %v", err) - } + err = pkg.WritePkg(outPath) } - return - } - switch inFile { - case "", "-": - default: - out, err := goListThenGet(g.BuildTags, inFile) if err != nil { - log.Fatalf("error:\n%s\n", out) + return cli.Exit(err, 1) + } + } else if inFile := c.String("in"); inFile != "" { + if inFile != "-" && inFile != "/dev/stdin" { + out, err := goListThenGet(c, g.BuildTags, inFile) + if err != nil { + return cli.Exit(err, 2) + } + inFile = out } - pf, err := g.Parse(out, nil) + pf, err := g.Parse(inFile, nil) if err != nil { - log.Fatalf("error parsing file (%s): %v\n%s", inFile, err, pf.Src) + return cli.Exit(fmt.Sprintf("error parsing file (%s): %v\n%s", inFile, err, pf.Src), 1) } + if err := pf.WriteFile(outPath); err != nil { - log.Fatalf("error writing file: %v", err) + return cli.Exit(err, 1) } + } else { + log.Println(c.FlagNames()) + // cli.ShowAppHelpAndExit(c, 1) } -} -func execCmd(c string, args ...string) (string, error) { + return nil +} +func execCmd(ctx *cli.Context, c string, args ...string) (string, error) { cmd := exec.Command(c, args...) - if verbose { + if ctx.Bool("verbose") { log.Printf("executing: %s %s", c, strings.Join(args, " ")) } out, err := cmd.CombinedOutput() return strings.TrimSpace(string(out)), err } -func goListThenGet(tags []string, path string) (out string, err error) { +func goListThenGet(ctx *cli.Context, tags []string, path string) (out string, err error) { if _, err = os.Stat(path); err == nil { return path, nil } @@ -224,21 +275,19 @@ func goListThenGet(tags []string, path string) (out string, err error) { } args := []string{"-tags", strings.Join(tags, " ")} - if goFlags != "" { - args = append(args, strings.Split(goFlags, " ")...) - } + args = append(args, ctx.StringSlice("goFlags")...) args = append(args, dir) listArgs := append([]string{"list", "-f", "{{.Dir}}"}, args...) - if out, err = execCmd("go", listArgs...); err != nil && strings.Contains(out, "cannot find package") { - if !goGet { - out = fmt.Sprintf("`%s` not found and `-get` isn't specified.", path) + if out, err = execCmd(ctx, "go", listArgs...); err != nil && strings.Contains(out, "cannot find package") { + if !ctx.Bool("get") { + out = fmt.Sprintf("`%s` not found and `--get` isn't specified.", path) return } - if out, err = execCmd("go", append([]string{"get", "-u", "-v"}, args...)...); err == nil && isFile { - out, err = execCmd("go", listArgs...) + if out, err = execCmd(ctx, "go", append([]string{"get", "-u", "-v"}, args...)...); err == nil && isFile { + out, err = execCmd(ctx, "go", listArgs...) } } @@ -247,8 +296,3 @@ func goListThenGet(tags []string, path string) (out string, err error) { } return } - -/* -go run cmd/genx/main.go -t KT=string -t "VT=interface{}" -rm-field HashFn -s "cm.HashFn=hashers.Fnv32" -s "cmap=-" -pkg ../cmap/internal/cmap/ - - n stringcmap -m -*/ diff --git a/genx.go b/genx.go index 9c41b23..dd8799c 100644 --- a/genx.go +++ b/genx.go @@ -10,6 +10,7 @@ import ( "go/token" "log" "path/filepath" + "reflect" "regexp" "strings" @@ -19,28 +20,50 @@ import ( "golang.org/x/tools/imports" ) +type procFunc func(*xast.Node) *xast.Node type GenX struct { pkgName string rewriters map[string]string irepl *strings.Replacer imports map[string]string - zero_types map[string]bool + zeroTypes map[string]bool curReturnTypes []string visited map[ast.Node]bool BuildTags []string CommentFilters []func(string) string + + rewriteFuncs map[reflect.Type][]procFunc } func New(pkgName string, rewriters map[string]string) *GenX { g := &GenX{ - pkgName: pkgName, - rewriters: map[string]string{}, - imports: map[string]string{}, - visited: map[ast.Node]bool{}, - irepl: geireplacer(rewriters, true), - zero_types: map[string]bool{}, - BuildTags: []string{"genx"}, + pkgName: pkgName, + rewriters: map[string]string{}, + imports: map[string]string{}, + visited: map[ast.Node]bool{}, + irepl: geireplacer(rewriters, true), + zeroTypes: map[string]bool{}, + BuildTags: []string{"genx"}, + } + + g.rewriteFuncs = map[reflect.Type][]procFunc{ + reflect.TypeOf((*ast.TypeSpec)(nil)): {g.rewriteTypeSpec}, + reflect.TypeOf((*ast.Ident)(nil)): {g.rewriteIdent}, + reflect.TypeOf((*ast.Field)(nil)): {g.rewriteField}, + reflect.TypeOf((*ast.FuncDecl)(nil)): {g.rewriteFuncDecl}, + reflect.TypeOf((*ast.File)(nil)): {g.rewriteFile}, + reflect.TypeOf((*ast.Comment)(nil)): {g.rewriteComment}, + reflect.TypeOf((*ast.SelectorExpr)(nil)): {g.rewriteSelectorExpr}, + reflect.TypeOf((*ast.KeyValueExpr)(nil)): {g.rewriteKeyValueExpr}, + reflect.TypeOf((*ast.InterfaceType)(nil)): {g.rewriteInterfaceType}, + reflect.TypeOf((*ast.ReturnStmt)(nil)): {g.rewriteReturnStmt}, + reflect.TypeOf((*ast.ArrayType)(nil)): {g.rewriteArrayType}, + reflect.TypeOf((*ast.ChanType)(nil)): {g.rewriteChanType}, + reflect.TypeOf((*ast.MapType)(nil)): {g.rewriteMapType}, + reflect.TypeOf((*ast.FuncType)(nil)): {g.rewriteFuncType}, + reflect.TypeOf((*ast.StarExpr)(nil)): {g.rewriteStarExpr}, + reflect.TypeOf((*ast.Ellipsis)(nil)): {g.rewriteEllipsis}, } for k, v := range rewriters { @@ -69,7 +92,7 @@ func New(pkgName string, rewriters map[string]string) *GenX { g.BuildTags = append(g.BuildTags, "genx_"+strings.ToLower(kw)+"_builtin") } g.BuildTags = append(g.BuildTags, "genx_"+strings.ToLower(kw)+"_"+csel) - g.zero_types[sel] = false + g.zeroTypes[sel] = false g.CommentFilters = append(g.CommentFilters, regexpReplacer(`\b(`+kw+`)\b`, sel)) g.CommentFilters = append(g.CommentFilters, regexpReplacer(`(`+kw+`)`, strings.Title(csel))) } @@ -78,7 +101,6 @@ func New(pkgName string, rewriters map[string]string) *GenX { g.rewriters[k] = sel } - g.CommentFilters = append(g.CommentFilters, regexpReplacer(`\+build \!?genx.*|go:generate genx`, "")) return g } @@ -152,9 +174,9 @@ func (g *GenX) process(idx int, fset *token.FileSet, name string, file *ast.File return } - if idx == 0 && len(g.zero_types) > 0 { + if idx == 0 && len(g.zeroTypes) > 0 { buf.WriteByte('\n') - for t, used := range g.zero_types { + for t, used := range g.zeroTypes { if used { fmt.Fprintf(&buf, "var zero_%s %s\n", cleanUpName.ReplaceAllString(t, ""), t) } @@ -176,183 +198,56 @@ func (g *GenX) process(idx int, fset *token.FileSet, name string, file *ast.File func (g *GenX) rewrite(node *xast.Node) *xast.Node { n := node.Node() if g.visited[n] { + //dbg.DumpWithDepth(4, n) + // log.Printf("%T %#+v", n, node.Parent().Node()) return node } g.visited[n] = true - rewr := g.rewriters - switch n := n.(type) { - case *ast.TypeSpec: - if t := getIdent(n.Name); t != nil { - nn, ok := rewr["type:"+t.Name] - if !ok { - break - } - if nn == "-" || nn == "" { - return node.Delete() - } - switch n.Type.(type) { - case *ast.SelectorExpr, *ast.InterfaceType, *ast.Ident: - return node.Delete() - default: - t.Name = nn - } - - } - - case *ast.FuncDecl: - if t := getIdent(n.Name); t != nil { - nn := rewr["func:"+t.Name] - if nn == "-" { - return node.Delete() - } else if nn != "" { - t.Name = nn - } - } - - if recv := n.Recv; recv != nil && len(recv.List) == 1 { - t := getIdent(recv.List[0].Type) - if t == nil { - log.Panicf("hmm... %#+v", recv.List[0].Type) - } - nn, ok := rewr["type:"+t.Name] - - if nn == "-" { - return node.Delete() - } - if ok { - t.Name = nn - } else { - t.Name = g.irepl.Replace(t.Name) - } - } - - if t, ok := g.rewriteExprTypes("type:", n.Type).(*ast.FuncType); ok { - n.Type = t - } else { - return node.Delete() - } - - if g.shouldNukeFuncBody(n.Body) { - return node.Delete() - } - - case *ast.Ident: - if t, ok := rewr["type:"+n.Name]; ok { - if t == "-" { + if fns, ok := g.rewriteFuncs[reflect.TypeOf(n)]; ok { + for _, fn := range fns { + if node = fn(node); node.Canceled() { break } - n.Name = t - } else { - n.Name = g.irepl.Replace(n.Name) - } - - case *ast.Field: - n.Type = g.rewriteExprTypes("type:", n.Type) - - if len(n.Names) == 0 { - break - } - - names := n.Names[:0] - for _, n := range n.Names { - nn, ok := rewr["field:"+n.Name] - if nn == "-" { - continue - } - if ok { - n.Name = nn - } else { - n.Name = g.irepl.Replace(n.Name) - } - names = append(names, n) - - } - - if n.Names = names; len(n.Names) == 0 { - return node.Delete() - } - - case *ast.Comment: - for _, f := range g.CommentFilters { - if n.Text = f(n.Text); n.Text == "" { - return node.Delete() - } - } - - case *ast.KeyValueExpr: - if key := getIdent(n.Key); key != nil && rewr["field:"+key.Name] == "-" { - return node.Delete() - } - - case *ast.SelectorExpr: - if x := getIdent(n.X); x != nil && n.Sel != nil { - if nv := g.rewriters["selector:."+n.Sel.Name]; nv != "" { - n.Sel.Name = nv - break - } - nv := g.rewriters["selector:"+x.Name+"."+n.Sel.Name] - if nv == "" { - if x.Name == g.pkgName { - x.Name = n.Sel.Name - return node.SetNode(x) - } - x.Name, n.Sel.Name = g.irepl.Replace(x.Name), g.irepl.Replace(n.Sel.Name) - break - } - if nv == "-" { - return node.Delete() - } - if xsel := strings.Split(nv, "."); len(xsel) == 2 { - x.Name, n.Sel.Name = xsel[0], xsel[1] - break - } else { - x.Name = nv - return node.SetNode(x) - } - - } - case *ast.InterfaceType: - if n.Methods != nil && len(n.Methods.List) == 0 { - if nt := g.rewriters["type:interface{}"]; nt != "" { - return node.SetNode(&ast.Ident{ - Name: nt, - }) - } - } - case *ast.ReturnStmt: - for i, r := range n.Results { - if rt := getIdent(r); rt != nil && rt.Name == "nil" { - crt := cleanUpName.ReplaceAllString(g.curReturnTypes[i], "") - if _, ok := g.zero_types[crt]; ok { - g.zero_types[crt] = true - rt.Name = "zero_" + cleanUpName.ReplaceAllString(crt, "") - } - } } } return node } -func indexOf(ss []string, v string) int { - for i, s := range ss { - if s == v { - return i - } - } - return -1 -} - func (g *GenX) shouldNukeFuncBody(bs *ast.BlockStmt) (found bool) { if bs == nil { return } + ast.Inspect(bs, func(n ast.Node) bool { if found { return false } switch n := n.(type) { + // BUG: maybe? should we delete the func if we remove a field? + case *ast.KeyValueExpr: + x := getIdent(n.Key) + if x == nil { + break + } + if found = g.rewriters["field:"+x.Name] == "-"; found { + return false + } + case *ast.SelectorExpr: + x := getIdent(n.X) + if x == nil { + break + } + if x.Obj != nil && x.Obj.Type != nil { + ot := getIdent(x.Obj.Type) + if found = ot != nil && g.rewriters["type:"+ot.Name] == "-"; found { + return false + } + } + if found = g.rewriters["field:"+n.Sel.Name] == "-"; found { + return false + } case *ast.Ident: if found = g.rewriters["type:"+n.Name] == "-"; found { return false @@ -364,74 +259,7 @@ func (g *GenX) shouldNukeFuncBody(bs *ast.BlockStmt) (found bool) { return } -func (g *GenX) rewriteExprTypes(prefix string, ex ast.Expr) ast.Expr { - if g.visited[ex] { - return ex - } - g.visited[ex] = true - - switch t := ex.(type) { - case *ast.InterfaceType: - if nt := g.rewriters[prefix+"interface{}"]; nt != "" { - if nt == "-" { - return nil - } - ex = &ast.Ident{ - Name: nt, - } - } - case *ast.Ident: - if nt := g.rewriters[prefix+t.Name]; nt != "" { - if nt == "-" { - return nil - } - t.Name = nt - } else { - t.Name = g.irepl.Replace(t.Name) - } - case *ast.StarExpr: - if t.X = g.rewriteExprTypes(prefix, t.X); t.X == nil { - return nil - } - case *ast.Ellipsis: - if t.Elt = g.rewriteExprTypes(prefix, t.Elt); t.Elt == nil { - return nil - } - case *ast.ArrayType: - if t.Elt = g.rewriteExprTypes(prefix, t.Elt); t.Elt == nil { - return nil - } - case *ast.MapType: - if t.Key = g.rewriteExprTypes(prefix, t.Key); t.Key == nil { - return nil - } - if t.Value = g.rewriteExprTypes(prefix, t.Value); t.Value == nil { - return nil - } - case *ast.FuncType: - if t.Params != nil { - for _, p := range t.Params.List { - if p.Type = g.rewriteExprTypes(prefix, p.Type); p.Type == nil { - return nil - } - } - } - if t.Results != nil { - g.curReturnTypes = g.curReturnTypes[:0] - for _, p := range t.Results.List { - if p.Type = g.rewriteExprTypes(prefix, p.Type); p.Type == nil { - return nil - } - if rt := getIdent(p.Type); rt != nil { - g.curReturnTypes = append(g.curReturnTypes, rt.Name) - } - } - } - } - return ex -} - -func getIdent(ex ast.Expr) *ast.Ident { +func getIdent(ex interface{}) *ast.Ident { switch ex := ex.(type) { case *ast.Ident: return ex @@ -462,33 +290,6 @@ func geireplacer(m map[string]string, ident bool) *strings.Replacer { return strings.NewReplacer(kv...) } -var replRe = regexp.MustCompile(`\b([\w\d_]+)\b`) - -func reRepl(m map[string]string, ident bool) func(src string) string { - kv := map[string]string{} - for k, v := range m { - k = k[strings.Index(k, ":")+1:] - if ident { - if a := builtins[v]; a != "" { - v = a - } else { - v = cleanUpName.ReplaceAllString(strings.Title(v), "") - } - } - - kv[k] = v - } - return func(src string) string { - re := replRe.Copy() - return re.ReplaceAllStringFunc(src, func(in string) string { - if v := kv[in]; v != "" { - return v - } - return in - }) - } -} - var builtins = map[string]string{ "string": "String", "byte": "Byte", diff --git a/rewriters.go b/rewriters.go new file mode 100644 index 0000000..c9a8f51 --- /dev/null +++ b/rewriters.go @@ -0,0 +1,319 @@ +package genx + +import ( + "go/ast" + "strings" + + "github.com/OneOfOne/xast" +) + +func (g *GenX) rewriteField(node *xast.Node) *xast.Node { + n := node.Node().(*ast.Field) + nn := g.rewrite(xast.NewNode(node, n.Type)) + if nn.Canceled() { + return node.Delete() + } + n.Type = nn.Node().(ast.Expr) + + if len(n.Names) == 0 { + return node + } + + names := n.Names[:0] + for _, n := range n.Names { + nn, ok := g.rewriters["field:"+n.Name] + if nn == "-" { + continue + } + if ok { + n.Name = nn + } else { + n.Name = g.irepl.Replace(n.Name) + } + names = append(names, n) + + } + if n.Names = names; len(n.Names) == 0 { + return node.Delete() + } + + return node +} + +func (g *GenX) rewriteTypeSpec(node *xast.Node) *xast.Node { + n := node.Node().(*ast.TypeSpec) + if t := getIdent(n.Name); t != nil { + nn := g.rewrite(xast.NewNode(node, n.Type)) + if nn.Canceled() { + return node.Delete() + } + n.Type = nn.Node().(ast.Expr) + tn, ok := g.rewriters["type:"+t.Name] + if !ok { + return node + } + if tn == "-" { + return node.Delete() + } + t.Name = tn + return node.Delete() + + } + return node +} + +func (g *GenX) rewriteIdent(node *xast.Node) *xast.Node { + n := node.Node().(*ast.Ident) + if t, ok := g.rewriters["type:"+n.Name]; ok { + if t == "-" { + return node.Delete() + } + n.Name = t + } else { + n.Name = g.irepl.Replace(n.Name) + } + return node +} + +func (g *GenX) rewriteArrayType(node *xast.Node) *xast.Node { + n := node.Node().(*ast.ArrayType) + nn := g.rewrite(xast.NewNode(node, n.Elt)) + if nn.Canceled() { + return deleteWithParent(node) + } + n.Elt = nn.Node().(ast.Expr) + return node + +} + +func (g *GenX) rewriteEllipsis(node *xast.Node) *xast.Node { + n := node.Node().(*ast.Ellipsis) + nn := g.rewrite(xast.NewNode(node, n.Elt)) + if nn.Canceled() { + return deleteWithParent(node) + } + n.Elt = nn.Node().(ast.Expr) + return node + +} + +func (g *GenX) rewriteStarExpr(node *xast.Node) *xast.Node { + n := node.Node().(*ast.StarExpr) + nn := g.rewrite(xast.NewNode(node, n.X)) + if nn.Canceled() { + return deleteWithParent(node) + } + n.X = nn.Node().(ast.Expr) + return node + +} + +func (g *GenX) rewriteChanType(node *xast.Node) *xast.Node { + n := node.Node().(*ast.ChanType) + if x := getIdent(n.Value); x != nil && g.rewriters["type:"+x.Name] == "-" { + return deleteWithParent(node) + } + return node +} + +func (g *GenX) rewriteFuncType(node *xast.Node) *xast.Node { + n := node.Node().(*ast.FuncType) + if n.Params != nil { + for _, p := range n.Params.List { + nn := g.rewrite(xast.NewNode(node, p.Type)) + if nn.Canceled() { + return node.Delete() + } + p.Type = nn.Node().(ast.Expr) + } + } + + if n.Results != nil { + g.curReturnTypes = g.curReturnTypes[:0] + for _, p := range n.Results.List { + nn := g.rewrite(xast.NewNode(node, p.Type)) + if nn.Canceled() { + return node.Delete() + } + p.Type = nn.Node().(ast.Expr) + if rt := getIdent(p.Type); rt != nil { + g.curReturnTypes = append(g.curReturnTypes, rt.Name) + } + } + } + + return node +} + +func (g *GenX) rewriteMapType(node *xast.Node) *xast.Node { + n := node.Node().(*ast.MapType) + nn := g.rewrite(xast.NewNode(node, n.Key)) + if nn.Canceled() { + return deleteWithParent(node) + } + n.Key = nn.Node().(ast.Expr) + nn = g.rewrite(xast.NewNode(node, n.Value)) + if nn.Canceled() { + return deleteWithParent(node) + } + n.Value = nn.Node().(ast.Expr) + return node +} + +func (g *GenX) rewriteComment(node *xast.Node) *xast.Node { + n := node.Node().(*ast.Comment) + for _, f := range g.CommentFilters { + if n.Text = f(n.Text); n.Text == "" { + return node.Delete() + } + } + return node +} + +func (g *GenX) rewriteKeyValueExpr(node *xast.Node) *xast.Node { + n := node.Node().(*ast.KeyValueExpr) + if t := getIdent(n.Key); t != nil { + if g.rewriters["type:"+t.Name] == "-" || g.rewriters["field:"+t.Name] == "-" { + return node.Delete() + } + } + + nn := g.rewrite(xast.NewNode(node, n.Value)) + if nn.Canceled() { + return node.Delete() + } + n.Value = nn.Node().(ast.Expr) + + return node +} + +func (g *GenX) rewriteReturnStmt(node *xast.Node) *xast.Node { + n := node.Node().(*ast.ReturnStmt) + for i, r := range n.Results { + if rt := getIdent(r); rt != nil && rt.Name == "nil" { + crt := cleanUpName.ReplaceAllString(g.curReturnTypes[i], "") + if _, ok := g.zeroTypes[crt]; ok { + g.zeroTypes[crt] = true + rt.Name = "zero_" + cleanUpName.ReplaceAllString(crt, "") + } + } + } + return node +} + +func (g *GenX) rewriteInterfaceType(node *xast.Node) *xast.Node { + n := node.Node().(*ast.InterfaceType) + if n.Methods != nil && len(n.Methods.List) == 0 { + if nt, ok := g.rewriters["type:interface{}"]; ok { + if nt == "-" { + return deleteWithParent(node) + } + return node.SetNode(&ast.Ident{ + Name: nt, + }) + } + } + return node +} + +func (g *GenX) rewriteSelectorExpr(node *xast.Node) *xast.Node { + n := node.Node().(*ast.SelectorExpr) + x := getIdent(n.X) + + if x == nil || n.Sel == nil { + return node + } + if nv := g.rewriters["selector:."+n.Sel.Name]; nv != "" { + n.Sel.Name = nv + return node + } + + nv := g.rewriters["selector:"+x.Name+"."+n.Sel.Name] + if nv == "" { + if x.Name == g.pkgName { + x.Name = n.Sel.Name + return node.SetNode(x) + } + x.Name, n.Sel.Name = g.irepl.Replace(x.Name), g.irepl.Replace(n.Sel.Name) + return node + } + + if nv == "-" { + return node.Delete() + } + + if xsel := strings.Split(nv, "."); len(xsel) == 2 { + x.Name, n.Sel.Name = xsel[0], xsel[1] + } else { + x.Name = nv + return node.SetNode(x) + } + + return node +} + +func (g *GenX) rewriteFuncDecl(node *xast.Node) *xast.Node { + n := node.Node().(*ast.FuncDecl) + if t := getIdent(n.Name); t != nil { + nn := g.rewriters["func:"+t.Name] + if nn == "-" { + return node.Delete() + } else if nn != "" { + t.Name = nn + } + } + + if recv := n.Recv; recv != nil && len(recv.List) == 1 { + t := getIdent(recv.List[0].Type) + nn, ok := g.rewriters["type:"+t.Name] + + if nn == "-" { + return node.Delete() + } + if ok { + t.Name = nn + } else { + t.Name = g.irepl.Replace(t.Name) + } + } + + if g.shouldNukeFuncBody(n.Body) { + return node.Delete() + } + + nn := g.rewrite(xast.NewNode(node, n.Type)) + if nn.Canceled() { + return node.Delete() + } + n.Type = nn.Node().(*ast.FuncType) + + return node +} + +var nukeGenxComments = regexpReplacer(`// \+build.*|//go:generate.*`, "") + +func (g *GenX) rewriteFile(node *xast.Node) *xast.Node { + n := node.Node().(*ast.File) + for _, cg := range n.Comments { + list := cg.List[:0] + for _, c := range cg.List { + if c.Text = nukeGenxComments(c.Text); c.Text != "" { + list = append(list, c) + } + } + cg.List = list + } + + if n.Doc == nil { + return node + } + + list := n.Doc.List[:0] + for _, c := range n.Doc.List { + if c.Text = nukeGenxComments(c.Text); c.Text != "" { + list = append(list, c) + } + } + n.Doc.List = list + return node +} diff --git a/rewriters_test.go b/rewriters_test.go new file mode 100644 index 0000000..ce32ecb --- /dev/null +++ b/rewriters_test.go @@ -0,0 +1,89 @@ +package genx_test + +import ( + "io/ioutil" + "log" + "regexp" + "testing" + + "github.com/OneOfOne/genx" +) + +func init() { + log.SetFlags(log.Lshortfile) +} +func TestAllTypes(t *testing.T) { + testCases := []struct { + Name string + Input map[string]string + FailIfMatch *regexp.Regexp + }{ + { + "Delete:Type:interface{}", + map[string]string{ + "type:interface{}": "-", + }, + regexp.MustCompile(`interface\{\}`), + }, + { + "Delete:Type:TypeWithKT", + map[string]string{"type:TypeWithKT": "-"}, + regexp.MustCompile(`TypeWithKT`), + }, + { + "Delete:Field:RemoveMe", + map[string]string{"field:RemoveMe": "-"}, + regexp.MustCompile(`RemoveMe`), + }, + { + "Rename:Field:Call", + map[string]string{"field:Call": "NewFuncName"}, + regexp.MustCompile(`\.Call`), + }, + { + "Type:interface{}=int", + map[string]string{ + "type:VT": "int", + "type:interface{}": "int", + }, + regexp.MustCompile(`interface\{\}`), + }, + { + "Type:KT=int,VT=int", + map[string]string{ + "type:KT": "uint64", + "type:VT": "int", + }, + regexp.MustCompile(`KT|VT`), + }, + { + "Func:DoStuff", + map[string]string{ + "func:DoStuff": "-", + }, + // regexp.MustCompile(`DoStuff\(|\sTwo\(`), // TODO: fix #5 + regexp.MustCompile(`DoStuff\(`), + }, + } + src, err := ioutil.ReadFile("./all_types.go") + fatalIf(t, err) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + g := genx.New("", tc.Input) + pf, err := g.Parse("src.go", src) + if err != nil { + t.Errorf("%+v\n%s", err, pf.Src) + return + } + if tc.FailIfMatch.Match(pf.Src) { + t.Errorf("%s matched :(\n%s", tc.FailIfMatch, pf.Src) + } + }) + } +} + +func fatalIf(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} diff --git a/utils.go b/utils.go index 5bcdabd..403255f 100644 --- a/utils.go +++ b/utils.go @@ -6,6 +6,8 @@ package genx import ( "regexp" "sort" + + "github.com/OneOfOne/xast" ) func (g *GenX) OrderedRewriters() (out []string) { @@ -49,3 +51,8 @@ func regexpReplacer(src string, repl string) func(string) string { return re.ReplaceAllString(in, repl) } } + +func deleteWithParent(n *xast.Node) *xast.Node { + n.Parent().Delete() + return n.Delete() +}