// keyify transforms unkeyed struct literals into a keyed ones. package main import ( "bytes" "encoding/json" "flag" "fmt" "go/ast" "go/build" "go/constant" "go/printer" "go/token" "go/types" "log" "os" "path/filepath" "honnef.co/go/tools/version" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" ) var ( fRecursive bool fOneLine bool fJSON bool fMinify bool fModified bool fVersion bool ) func init() { flag.BoolVar(&fRecursive, "r", false, "keyify struct initializers recursively") flag.BoolVar(&fOneLine, "o", false, "print new struct initializer on a single line") flag.BoolVar(&fJSON, "json", false, "print new struct initializer as JSON") flag.BoolVar(&fMinify, "m", false, "omit fields that are set to their zero value") flag.BoolVar(&fModified, "modified", false, "read an archive of modified files from standard input") flag.BoolVar(&fVersion, "version", false, "Print version and exit") } func usage() { fmt.Printf("Usage: %s [flags] \n\n", os.Args[0]) flag.PrintDefaults() } func main() { log.SetFlags(0) flag.Usage = usage flag.Parse() if fVersion { version.Print() os.Exit(0) } if flag.NArg() != 1 { flag.Usage() os.Exit(2) } pos := flag.Args()[0] name, start, _, err := parsePos(pos) if err != nil { log.Fatal(err) } eval, err := filepath.EvalSymlinks(name) if err != nil { log.Fatal(err) } name, err = filepath.Abs(eval) if err != nil { log.Fatal(err) } cwd, err := os.Getwd() if err != nil { log.Fatal(err) } ctx := &build.Default if fModified { overlay, err := buildutil.ParseOverlayArchive(os.Stdin) if err != nil { log.Fatal(err) } ctx = buildutil.OverlayContext(ctx, overlay) } bpkg, err := buildutil.ContainingPackage(ctx, cwd, name) if err != nil { log.Fatal(err) } conf := &loader.Config{ Build: ctx, } conf.TypeCheckFuncBodies = func(s string) bool { return s == bpkg.ImportPath || s == bpkg.ImportPath+"_test" } conf.ImportWithTests(bpkg.ImportPath) lprog, err := conf.Load() if err != nil { log.Fatal(err) } var tf *token.File var af *ast.File var pkg *loader.PackageInfo outer: for _, pkg = range lprog.InitialPackages() { for _, ff := range pkg.Files { file := lprog.Fset.File(ff.Pos()) if file.Name() == name { af = ff tf = file break outer } } } if tf == nil { log.Fatalf("couldn't find file %s", name) } tstart, tend, err := fileOffsetToPos(tf, start, start) if err != nil { log.Fatal(err) } path, _ := astutil.PathEnclosingInterval(af, tstart, tend) var complit *ast.CompositeLit for _, p := range path { if p, ok := p.(*ast.CompositeLit); ok { complit = p break } } if complit == nil { log.Fatal("no composite literal found near point") } if len(complit.Elts) == 0 { printComplit(complit, complit, lprog.Fset, lprog.Fset) return } if _, ok := complit.Elts[0].(*ast.KeyValueExpr); ok { lit := complit if fOneLine { lit = copyExpr(complit, 1).(*ast.CompositeLit) } printComplit(complit, lit, lprog.Fset, lprog.Fset) return } _, ok := pkg.TypeOf(complit).Underlying().(*types.Struct) if !ok { log.Fatal("not a struct initialiser") return } newComplit, lines := keyify(pkg, complit) newFset := token.NewFileSet() newFile := newFset.AddFile("", -1, lines) for i := 1; i <= lines; i++ { newFile.AddLine(i) } printComplit(complit, newComplit, lprog.Fset, newFset) } func keyify( pkg *loader.PackageInfo, complit *ast.CompositeLit, ) (*ast.CompositeLit, int) { var calcPos func(int) token.Pos if fOneLine { calcPos = func(int) token.Pos { return token.Pos(1) } } else { calcPos = func(i int) token.Pos { return token.Pos(2 + i) } } st, _ := pkg.TypeOf(complit).Underlying().(*types.Struct) newComplit := &ast.CompositeLit{ Type: complit.Type, Lbrace: 1, Rbrace: token.Pos(st.NumFields() + 2), } if fOneLine { newComplit.Rbrace = 1 } numLines := 2 + st.NumFields() n := 0 for i := 0; i < st.NumFields(); i++ { field := st.Field(i) val := complit.Elts[i] if fRecursive { if val2, ok := val.(*ast.CompositeLit); ok { if _, ok := pkg.TypeOf(val2.Type).Underlying().(*types.Struct); ok { // FIXME(dh): this code is obviously wrong. But // what were we intending to do here? var lines int numLines += lines //lint:ignore SA4006 See FIXME above. val, lines = keyify(pkg, val2) } } } _, isIface := st.Field(i).Type().Underlying().(*types.Interface) if fMinify && (isNil(val, pkg) || (!isIface && isZero(val, pkg))) { continue } elt := &ast.KeyValueExpr{ Key: &ast.Ident{NamePos: calcPos(n), Name: field.Name()}, Value: copyExpr(val, calcPos(n)), } newComplit.Elts = append(newComplit.Elts, elt) n++ } return newComplit, numLines } func isNil(val ast.Expr, pkg *loader.PackageInfo) bool { ident, ok := val.(*ast.Ident) if !ok { return false } if _, ok := pkg.ObjectOf(ident).(*types.Nil); ok { return true } if c, ok := pkg.ObjectOf(ident).(*types.Const); ok { if c.Val().Kind() != constant.Bool { return false } return !constant.BoolVal(c.Val()) } return false } func isZero(val ast.Expr, pkg *loader.PackageInfo) bool { switch val := val.(type) { case *ast.BasicLit: switch val.Value { case `""`, "``", "0", "0.0", "0i", "0.": return true default: return false } case *ast.Ident: return isNil(val, pkg) case *ast.CompositeLit: typ := pkg.TypeOf(val.Type) if typ == nil { return false } isIface := false switch typ := typ.Underlying().(type) { case *types.Struct: case *types.Array: _, isIface = typ.Elem().Underlying().(*types.Interface) default: return false } for _, elt := range val.Elts { if isNil(elt, pkg) || (!isIface && !isZero(elt, pkg)) { return false } } return true } return false } func printComplit(oldlit, newlit *ast.CompositeLit, oldfset, newfset *token.FileSet) { buf := &bytes.Buffer{} cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8} _ = cfg.Fprint(buf, newfset, newlit) if fJSON { output := struct { Start int `json:"start"` End int `json:"end"` Replacement string `json:"replacement"` }{ oldfset.Position(oldlit.Pos()).Offset, oldfset.Position(oldlit.End()).Offset, buf.String(), } _ = json.NewEncoder(os.Stdout).Encode(output) } else { fmt.Println(buf.String()) } } func copyExpr(expr ast.Expr, line token.Pos) ast.Expr { switch expr := expr.(type) { case *ast.BasicLit: cp := *expr cp.ValuePos = 0 return &cp case *ast.BinaryExpr: cp := *expr cp.X = copyExpr(cp.X, line) cp.OpPos = 0 cp.Y = copyExpr(cp.Y, line) return &cp case *ast.CallExpr: cp := *expr cp.Fun = copyExpr(cp.Fun, line) cp.Lparen = 0 for i, v := range cp.Args { cp.Args[i] = copyExpr(v, line) } if cp.Ellipsis != 0 { cp.Ellipsis = line } cp.Rparen = 0 return &cp case *ast.CompositeLit: cp := *expr cp.Type = copyExpr(cp.Type, line) cp.Lbrace = 0 for i, v := range cp.Elts { cp.Elts[i] = copyExpr(v, line) } cp.Rbrace = 0 return &cp case *ast.Ident: cp := *expr cp.NamePos = 0 return &cp case *ast.IndexExpr: cp := *expr cp.X = copyExpr(cp.X, line) cp.Lbrack = 0 cp.Index = copyExpr(cp.Index, line) cp.Rbrack = 0 return &cp case *ast.KeyValueExpr: cp := *expr cp.Key = copyExpr(cp.Key, line) cp.Colon = 0 cp.Value = copyExpr(cp.Value, line) return &cp case *ast.ParenExpr: cp := *expr cp.Lparen = 0 cp.X = copyExpr(cp.X, line) cp.Rparen = 0 return &cp case *ast.SelectorExpr: cp := *expr cp.X = copyExpr(cp.X, line) cp.Sel = copyExpr(cp.Sel, line).(*ast.Ident) return &cp case *ast.SliceExpr: cp := *expr cp.X = copyExpr(cp.X, line) cp.Lbrack = 0 cp.Low = copyExpr(cp.Low, line) cp.High = copyExpr(cp.High, line) cp.Max = copyExpr(cp.Max, line) cp.Rbrack = 0 return &cp case *ast.StarExpr: cp := *expr cp.Star = 0 cp.X = copyExpr(cp.X, line) return &cp case *ast.TypeAssertExpr: cp := *expr cp.X = copyExpr(cp.X, line) cp.Lparen = 0 cp.Type = copyExpr(cp.Type, line) cp.Rparen = 0 return &cp case *ast.UnaryExpr: cp := *expr cp.OpPos = 0 cp.X = copyExpr(cp.X, line) return &cp case *ast.MapType: cp := *expr cp.Map = 0 cp.Key = copyExpr(cp.Key, line) cp.Value = copyExpr(cp.Value, line) return &cp case *ast.ArrayType: cp := *expr cp.Lbrack = 0 cp.Len = copyExpr(cp.Len, line) cp.Elt = copyExpr(cp.Elt, line) return &cp case *ast.Ellipsis: cp := *expr cp.Elt = copyExpr(cp.Elt, line) cp.Ellipsis = line return &cp case *ast.InterfaceType: cp := *expr cp.Interface = 0 return &cp case *ast.StructType: cp := *expr cp.Struct = 0 return &cp case *ast.FuncLit: return expr case *ast.ChanType: cp := *expr cp.Arrow = 0 cp.Begin = 0 cp.Value = copyExpr(cp.Value, line) return &cp case nil: return nil default: panic(fmt.Sprintf("shouldn't happen: unknown ast.Expr of type %T", expr)) } }