package prealloc import ( "errors" "flag" "fmt" "go/ast" "go/build" "go/parser" "go/token" "log" "os" "path/filepath" "strings" ) // Support: (in order of priority) // * Full make suggestion with type? // * Test flag // * Embedded ifs? // * Use an import rather than the duplcated import.go const ( pwd = "./" ) func usage() { log.Printf("Usage of %s:\n", os.Args[0]) log.Printf("\nprealloc [flags] # runs on package in current directory\n") log.Printf("\nprealloc [flags] [packages]\n") log.Printf("Flags:\n") flag.PrintDefaults() } type sliceDeclaration struct { name string // sType string genD *ast.GenDecl } type returnsVisitor struct { // flags simple bool includeRangeLoops bool includeForLoops bool // visitor fields sliceDeclarations []*sliceDeclaration preallocHints []Hint returnsInsideOfLoop bool arrayTypes []string } func NoMain() { // Remove log timestamp log.SetFlags(0) simple := flag.Bool("simple", true, "Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them") includeRangeLoops := flag.Bool("rangeloops", true, "Report preallocation suggestions on range loops") includeForLoops := flag.Bool("forloops", false, "Report preallocation suggestions on for loops") setExitStatus := flag.Bool("set_exit_status", false, "Set exit status to 1 if any issues are found") flag.Usage = usage flag.Parse() hints, err := checkForPreallocations(flag.Args(), simple, includeRangeLoops, includeForLoops) if err != nil { log.Println(err) } for _, hint := range hints { log.Println(hint) } if *setExitStatus && len(hints) > 0 { os.Exit(1) } } func checkForPreallocations(args []string, simple, includeRangeLoops *bool, includeForLoops *bool) ([]Hint, error) { fset := token.NewFileSet() files, err := parseInput(args, fset) if err != nil { return nil, fmt.Errorf("could not parse input %v", err) } if simple == nil { return nil, errors.New("simple nil") } if includeRangeLoops == nil { return nil, errors.New("includeRangeLoops nil") } if includeForLoops == nil { return nil, errors.New("includeForLoops nil") } hints := []Hint{} for _, f := range files { retVis := &returnsVisitor{ simple: *simple, includeRangeLoops: *includeRangeLoops, includeForLoops: *includeForLoops, } ast.Walk(retVis, f) // if simple is true, then we actually have to check if we had returns // inside of our loop. Otherwise, we can just report all messages. if !retVis.simple || !retVis.returnsInsideOfLoop { hints = append(hints, retVis.preallocHints...) } } return hints, nil } func Check(files []*ast.File, simple, includeRangeLoops, includeForLoops bool) []Hint { hints := []Hint{} for _, f := range files { retVis := &returnsVisitor{ simple: simple, includeRangeLoops: includeRangeLoops, includeForLoops: includeForLoops, } ast.Walk(retVis, f) // if simple is true, then we actually have to check if we had returns // inside of our loop. Otherwise, we can just report all messages. if !retVis.simple || !retVis.returnsInsideOfLoop { hints = append(hints, retVis.preallocHints...) } } return hints } func parseInput(args []string, fset *token.FileSet) ([]*ast.File, error) { var directoryList []string var fileMode bool files := make([]*ast.File, 0) if len(args) == 0 { directoryList = append(directoryList, pwd) } else { for _, arg := range args { if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-len("/...")]) { for _, dirname := range allPackagesInFS(arg) { directoryList = append(directoryList, dirname) } } else if isDir(arg) { directoryList = append(directoryList, arg) } else if exists(arg) { if strings.HasSuffix(arg, ".go") { fileMode = true f, err := parser.ParseFile(fset, arg, nil, 0) if err != nil { return nil, err } files = append(files, f) } else { return nil, fmt.Errorf("invalid file %v specified", arg) } } else { //TODO clean this up a bit imPaths := importPaths([]string{arg}) for _, importPath := range imPaths { pkg, err := build.Import(importPath, ".", 0) if err != nil { return nil, err } var stringFiles []string stringFiles = append(stringFiles, pkg.GoFiles...) // files = append(files, pkg.CgoFiles...) stringFiles = append(stringFiles, pkg.TestGoFiles...) if pkg.Dir != "." { for i, f := range stringFiles { stringFiles[i] = filepath.Join(pkg.Dir, f) } } fileMode = true for _, stringFile := range stringFiles { f, err := parser.ParseFile(fset, stringFile, nil, 0) if err != nil { return nil, err } files = append(files, f) } } } } } // if we're not in file mode, then we need to grab each and every package in each directory // we can to grab all the files if !fileMode { for _, fpath := range directoryList { pkgs, err := parser.ParseDir(fset, fpath, nil, 0) if err != nil { return nil, err } for _, pkg := range pkgs { for _, f := range pkg.Files { files = append(files, f) } } } } return files, nil } func isDir(filename string) bool { fi, err := os.Stat(filename) return err == nil && fi.IsDir() } func exists(filename string) bool { _, err := os.Stat(filename) return err == nil } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } func (v *returnsVisitor) Visit(node ast.Node) ast.Visitor { v.sliceDeclarations = nil v.returnsInsideOfLoop = false switch n := node.(type) { case *ast.TypeSpec: if _, ok := n.Type.(*ast.ArrayType); ok { if n.Name != nil { v.arrayTypes = append(v.arrayTypes, n.Name.Name) } } case *ast.FuncDecl: if n.Body != nil { for _, stmt := range n.Body.List { switch s := stmt.(type) { // Find non pre-allocated slices case *ast.DeclStmt: genD, ok := s.Decl.(*ast.GenDecl) if !ok { continue } if genD.Tok == token.TYPE { for _, spec := range genD.Specs { tSpec, ok := spec.(*ast.TypeSpec) if !ok { continue } if _, ok := tSpec.Type.(*ast.ArrayType); ok { if tSpec.Name != nil { v.arrayTypes = append(v.arrayTypes, tSpec.Name.Name) } } } } else if genD.Tok == token.VAR { for _, spec := range genD.Specs { vSpec, ok := spec.(*ast.ValueSpec) if !ok { continue } var isArrType bool switch val := vSpec.Type.(type) { case *ast.ArrayType: isArrType = true case *ast.Ident: isArrType = contains(v.arrayTypes, val.Name) } if isArrType { if vSpec.Names != nil { /*atID, ok := arrayType.Elt.(*ast.Ident) if !ok { continue }*/ // We should handle multiple slices declared on same line e.g. var mySlice1, mySlice2 []uint32 for _, vName := range vSpec.Names { v.sliceDeclarations = append(v.sliceDeclarations, &sliceDeclaration{name: vName.Name /*sType: atID.Name,*/, genD: genD}) } } } } } case *ast.RangeStmt: if v.includeRangeLoops { if len(v.sliceDeclarations) == 0 { continue } if s.Body != nil { v.handleLoops(s.Body) } } case *ast.ForStmt: if v.includeForLoops { if len(v.sliceDeclarations) == 0 { continue } if s.Body != nil { v.handleLoops(s.Body) } } default: } } } } return v } // handleLoops is a helper function to share the logic required for both *ast.RangeLoops and *ast.ForLoops func (v *returnsVisitor) handleLoops(blockStmt *ast.BlockStmt) { for _, stmt := range blockStmt.List { switch bodyStmt := stmt.(type) { case *ast.AssignStmt: asgnStmt := bodyStmt for _, expr := range asgnStmt.Rhs { callExpr, ok := expr.(*ast.CallExpr) if !ok { continue // should this be break? comes back to multi-call support I think } ident, ok := callExpr.Fun.(*ast.Ident) if !ok { continue } if ident.Name == "append" { // see if this append is appending the slice we found for _, lhsExpr := range asgnStmt.Lhs { lhsIdent, ok := lhsExpr.(*ast.Ident) if !ok { continue } for _, sliceDecl := range v.sliceDeclarations { if sliceDecl.name == lhsIdent.Name { // This is a potential mark, we just need to make sure there are no returns/continues in the // range loop. // now we just need to grab whatever we're ranging over /*sxIdent, ok := s.X.(*ast.Ident) if !ok { continue }*/ v.preallocHints = append(v.preallocHints, Hint{ Pos: sliceDecl.genD.Pos(), DeclaredSliceName: sliceDecl.name, }) } } } } } case *ast.IfStmt: ifStmt := bodyStmt if ifStmt.Body != nil { for _, ifBodyStmt := range ifStmt.Body.List { // TODO should probably handle embedded ifs here switch /*ift :=*/ ifBodyStmt.(type) { case *ast.BranchStmt, *ast.ReturnStmt: v.returnsInsideOfLoop = true default: } } } default: } } } // Hint stores the information about an occurence of a slice that could be // preallocated. type Hint struct { Pos token.Pos DeclaredSliceName string } func (h Hint) String() string { return fmt.Sprintf("%v: Consider preallocating %v", h.Pos, h.DeclaredSliceName) }