[
  {
    "path": ".gitignore",
    "content": "copyfighter\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\ngo:\n  - 1.6.2\n\n\ninstall:\n  - go test -i .\n\nscript:\n  - go test -v .\n"
  },
  {
    "path": "README.md",
    "content": "copyfighter\n===========\n\nCopyfighter statically analyzes Go code and reports functions that are passing\nlarge structs by value. It helps you help your code.\n\nEvery Go function call copies the values given to it, including structs. When\nlarge structs are passed around without using a pointer to them, the copying of\nnew data in memory causes more allocations and more work for your garbage\ncollector.\n\nCopyfighter's static analysis will identify where large structs, without\npointers, are being used as method receivers, function parameters and return\nvalues.\n\nInstall with `go get` or similar.\n\nExample output\n---------------\n    $ copyfighter /gopath/path/to/pkg\n    # parameter 'f' at index 0 should be made into a pointer\n    func CallsFoo(f Foo)\n    \n    # receiver, and parameter 'o' at index 0 should be made into pointers\n    func (Foo).OnOtherToo(o other)\n    \n    # receiver should be made into a pointer\n    func (other).OnStruct()\n    \n    # receiver should be made into a pointer\n    func (other).OnStruct2()\n\n    $ copyfighter -max 32 path/to/pkg\n    # parameter 'f' at index 0 should be made into a pointer\n    func CallsFoo(f Foo)\n\n    # receiver should be made into a pointer\n    func (Foo).OnOtherToo(o other)\n\nDefaults And Flags\n------------------\n\nBy default, copyfighter assumes structs wider than 16 bytes (two words on x86\\_64) should\nnot be copied. This can be adjusted with the `-max` flag. `max` should typically\nbe set to some multiple of the word size. You can also adjust the word size and alignment offset for your preferred architecture with `-wordSize` and `-maxAlign`.\n\nFlags like `-max` have to go before the package name.\n\nFAQ\n---\n\nWhy not just use Go's heap profiler to fix these problems when they show up?\n\nBecause copyfighter can find problems before you put your code in production. It's nice to prevent issues before they matter.\n"
  },
  {
    "path": "check_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestGoldenPath(t *testing.T) {\n\tsites, fset, err := check(\"./testdata\", 16, 8, 8)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %s\", err)\n\t}\n\tb := &bytes.Buffer{}\n\tprintSites(sites, fset, b)\n\tactual := string(b.Bytes())\n\tif goldenData != actual {\n\t\tt.Errorf(\"output doesn't match, want:\\n%s\\n=============\\ngot:\\n%s\", goldenData, actual)\n\t}\n}\n\nconst goldenData = `testdata/inner.go:24:6: parameter 'f' at index 0 should be made into a pointer (func CallsFoo(f Foo))\ntestdata/inner.go:28:14: receiver, and parameter 'o' at index 0 should be made into pointers (func (Foo).OnOtherToo(o other))\ntestdata/inner.go:32:16: receiver should be made into a pointer (func (other).OnStruct())\ntestdata/inner.go:35:16: receiver should be made into a pointer (func (other).OnStruct2())\n`\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/build\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"go/types\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/gcexportdata\"\n)\n\nvar (\n\tmaxStructWidth = flag.Int64(\"max\", 16, \"maximum size in bytes a struct can be before by-value uses are flagged\")\n\twordSize       = flag.Int64(\"wordSize\", 8, \"word size to assume when calculation struct size\")\n\tmaxAlign       = flag.Int64(\"maxAlign\", 8, \"maximum word alignment to assume when calculating struct size\")\n)\n\nfunc main() {\n\tlog.SetPrefix(\"\")\n\tlog.SetFlags(0)\n\tflag.Parse()\n\n\tif flag.NArg() != 1 {\n\t\tlog.Fatalf(\"usage: %s GO_PKG_DIR\", os.Args[0])\n\t}\n\tp := flag.Arg(0)\n\tsites, fset, err := check(p, *maxStructWidth, *wordSize, *maxAlign)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tprintSites(sites, fset, os.Stdout)\n\tif len(sites) > 0 {\n\t\tos.Exit(2)\n\t}\n\n}\n\nfunc check(p string, maxStructWidth, wordSize, maxAlign int64) ([]copySite, *token.FileSet, error) {\n\tfset := token.NewFileSet()\n\n\t_, err := os.Stat(p)\n\tswitch {\n\tcase os.IsNotExist(err):\n\t\t// File doesn't exist, probably a Go import path\n\t\tpkgs, err := parseGoPkg(p, fset)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tsites := []copySite{}\n\t\tfor _, pkg := range pkgs {\n\t\t\ts, err := checkPkg(pkg, fset, maxStructWidth, wordSize, maxAlign)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tsites = append(sites, s...)\n\t\t}\n\t\treturn sites, fset, nil\n\tcase err == nil:\n\t\t// File exists, parses as such\n\t\tpkg, err := parsePkgDir(p, fset)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tsites, err := checkPkg(pkg, fset, maxStructWidth, wordSize, maxAlign)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn sites, fset, nil\n\tdefault:\n\t\treturn nil, nil, err\n\t}\n}\n\nfunc parsePkgDir(p string, fset *token.FileSet) (*ast.Package, error) {\n\tfi, err := os.Stat(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to stat file %#v: %s\", p, err)\n\t}\n\tif !fi.IsDir() {\n\t\treturn nil, fmt.Errorf(\"%#v is not a directory\", p)\n\t}\n\n\tbuildContext := build.Default\n\tbpkg, err := buildContext.ImportDir(p, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse package at %#v: %s\", p, err)\n\t}\n\n\tmp, err := parser.ParseDir(fset, p, func(i os.FileInfo) bool {\n\t\tfor _, f := range bpkg.IgnoredGoFiles {\n\t\t\tif f == i.Name() {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\tfor _, f := range bpkg.InvalidGoFiles {\n\t\t\tif f == i.Name() {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse package at %#v: %s\", p, err)\n\t}\n\tif len(mp) != 1 {\n\t\tvar ps []string\n\t\tfor _, pkg := range mp {\n\t\t\tps = append(ps, pkg.Name)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"more than one package found in %#v: %s\", p, strings.Join(ps, \",\"))\n\t}\n\tvar pkg *ast.Package\n\tfor _, v := range mp {\n\t\tpkg = v\n\t}\n\treturn pkg, nil\n}\n\nfunc pathToRegexp(p string) *regexp.Regexp {\n\tre := regexp.QuoteMeta(p)\n\tre = strings.Replace(re, `\\.\\.\\.`, `.*`, -1)\n\t// Special case: foo/... matches foo too.\n\tif strings.HasSuffix(re, `/.*`) {\n\t\tre = re[:len(re)-len(`/.*`)] + `(/.*)?`\n\t}\n\treturn regexp.MustCompile(`^` + re + `$`)\n}\n\nfunc parseGoPkg(p string, fset *token.FileSet) ([]*ast.Package, error) {\n\tp = filepath.Clean(p)\n\tdirs := []string{}\n\tre := pathToRegexp(p)\n\tbuildContext := build.Default\n\tfor _, src := range buildContext.SrcDirs() {\n\t\tsrc = filepath.Clean(src) + string(filepath.Separator)\n\t\troot := src\n\t\tfilepath.Walk(root, func(path string, fi os.FileInfo, err error) error {\n\t\t\tif err != nil || !fi.IsDir() || path == src {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Avoid .foo, _foo, and testdata directory trees.\n\t\t\t_, elem := filepath.Split(path)\n\t\t\tif strings.HasPrefix(elem, \".\") || strings.HasPrefix(elem, \"_\") || elem == \"testdata\" {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\t\t\tname := filepath.ToSlash(path[len(src):])\n\t\t\tif re.MatchString(name) {\n\t\t\t\tdirs = append(dirs, path)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tpkgs := []*ast.Package{}\n\tfor _, d := range dirs {\n\t\t_, err := buildContext.ImportDir(d, 0)\n\t\tif err != nil {\n\t\t\tif _, noGo := err.(*build.NoGoError); noGo {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"unable to build code in %#v: %s\", d, err)\n\t\t}\n\t\tpkg, err := parsePkgDir(d, fset)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpkgs = append(pkgs, pkg)\n\t}\n\tif len(pkgs) == 0 {\n\t\treturn nil, fmt.Errorf(\"unable to find packages matching %#v\", p)\n\t}\n\n\treturn pkgs, nil\n}\n\nfunc checkPkg(pkg *ast.Package, fset *token.FileSet, maxWidth, wordSize, maxAlign int64) ([]copySite, error) {\n\tsizes := &types.StdSizes{WordSize: wordSize, MaxAlign: maxAlign}\n\tinfo := &types.Info{\n\t\t// Types is required to prevent duplicates, it seems, in Defs.\n\t\tTypes: make(map[ast.Expr]types.TypeAndValue),\n\t\tDefs:  make(map[*ast.Ident]types.Object),\n\t}\n\n\tconf := &types.Config{\n\t\tImporter:                 gcexportdata.NewImporter(fset, make(map[string]*types.Package)),\n\t\tDisableUnusedImportCheck: true,\n\t\tSizes:                    sizes,\n\t}\n\tfiles := []*ast.File{}\n\tfor _, f := range pkg.Files {\n\t\tfiles = append(files, f)\n\t}\n\n\t_, err := conf.Check(\"\", fset, files, info)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to type check package %#v: %s\", pkg.Name, err)\n\t}\n\n\twideStructs := make(map[string]bool)\n\n\tfuncs := []*types.Func{}\n\tfor _, obj := range info.Defs {\n\t\tif tn, ok := obj.(*types.TypeName); ok {\n\t\t\tif sizes.Sizeof(tn.Type()) > maxWidth {\n\t\t\t\twideStructs[tn.Id()] = true\n\t\t\t}\n\t\t}\n\t\tif f, ok := obj.(*types.Func); ok {\n\t\t\tfuncs = append(funcs, f)\n\t\t}\n\t}\n\n\tsites := findCopySites(funcs, wideStructs)\n\n\treturn sites, nil\n}\n\n// findCopySites returns a slice of copySites that represent Go function calls\n// that use a large struct without a pointer to it. The wideStructs argument is\n// a map of the struct's TypeName id to its TypeName object.\nfunc findCopySites(funcs []*types.Func, wideStructs map[string]bool) []copySite {\n\tsites := []copySite{}\n\tfor _, f := range funcs {\n\t\ts := f.Type().(*types.Signature)\n\t\tshouldBe := []string{}\n\n\t\t// If the func is a method, check the receiver\n\t\tif s.Recv() != nil {\n\t\t\trt := s.Recv().Type()\n\t\t\tif isWideStructTyped(rt, wideStructs) {\n\t\t\t\tshouldBe = append(shouldBe, \"receiver\")\n\t\t\t}\n\t\t}\n\n\t\tparams := s.Params()\n\t\tfor i := 0; i < params.Len(); i++ {\n\t\t\tv := params.At(i)\n\t\t\tif isWideStructTyped(v.Type(), wideStructs) {\n\t\t\t\tname := v.Name()\n\t\t\t\tparameter := \"parameter\"\n\t\t\t\tif name != \"\" {\n\t\t\t\t\tparameter = fmt.Sprintf(\"parameter '%s'\", name)\n\t\t\t\t}\n\t\t\t\tshouldBe = append(shouldBe,\n\t\t\t\t\tfmt.Sprintf(\"%s at index %d\", parameter, i))\n\t\t\t}\n\t\t}\n\n\t\tresults := s.Results()\n\t\tfor i := 0; i < results.Len(); i++ {\n\t\t\tv := results.At(i)\n\t\t\tif isWideStructTyped(v.Type(), wideStructs) {\n\t\t\t\tshouldBe = append(shouldBe,\n\t\t\t\t\tfmt.Sprintf(\"return value '%s' at index %d\", v.Type(), i))\n\t\t\t}\n\t\t}\n\t\tif len(shouldBe) > 0 {\n\t\t\tsites = append(sites, copySite{f, shouldBe})\n\t\t}\n\t}\n\treturn sites\n}\n\nfunc printSites(sites []copySite, fset *token.FileSet, w io.Writer) {\n\tsort.Sort(sortedCopySites{sites: sites, fset: fset})\n\tfor _, site := range sites {\n\t\tf := site.fun\n\t\tshouldBe := site.shouldBe\n\t\tsb := sentence(shouldBe)\n\t\tmsg := \"should be made into\"\n\t\tif len(shouldBe) > 1 {\n\t\t\tmsg += \" pointers\"\n\t\t} else {\n\t\t\tmsg += \" a pointer\"\n\t\t}\n\t\tpos := site.fun.Pos()\n\t\tfile := fset.File(pos)\n\t\tposition := file.Position(pos)\n\t\tfmt.Fprintf(w, \"%s:%d:%d: %s %s (%s)\\n\", file.Name(), position.Line, position.Column, sb, msg, f)\n\t}\n}\n\ntype copySite struct {\n\tfun      *types.Func\n\tshouldBe []string\n}\n\n// sortedCopySites sorts copySites as ordered by the filename, line, and column\n// the func was found at.\ntype sortedCopySites struct {\n\tsites []copySite\n\tfset  *token.FileSet\n}\n\nfunc (s sortedCopySites) Len() int {\n\treturn len(s.sites)\n}\nfunc (s sortedCopySites) Swap(i, j int) {\n\ts.sites[i], s.sites[j] = s.sites[j], s.sites[i]\n}\n\nfunc (s sortedCopySites) Less(i, j int) bool {\n\tleft := s.fset.Position(s.sites[i].fun.Pos())\n\tright := s.fset.Position(s.sites[j].fun.Pos())\n\n\tif left.Filename != right.Filename {\n\t\treturn left.Filename < right.Filename\n\t}\n\tif left.Line != right.Line {\n\t\treturn left.Line < right.Line\n\t}\n\treturn left.Column < right.Column\n}\n\n// isWideStructTyped returns true if the given type is a struct (not a pointer to\n// a struct) that is in wideStructs.\nfunc isWideStructTyped(t types.Type, wideStructs map[string]bool) bool {\n\tif named, ok := t.(*types.Named); ok {\n\t\treturn wideStructs[named.Obj().Id()]\n\t}\n\treturn false\n}\n\nfunc sentence(parts []string) string {\n\tif len(parts) == 0 {\n\t\treturn \"\"\n\t}\n\tif len(parts) == 1 {\n\t\treturn parts[0]\n\t}\n\tlast := len(parts) - 1\n\treturn strings.Join(parts[:last], \", \") + \", and \" + parts[last]\n}\n"
  },
  {
    "path": "testdata/inner.go",
    "content": "package main\n\nimport \"net/http\"\n\ntype someInt interface {\n\tBang()\n}\ntype bar struct {\n\tbaz int\n}\n\ntype other struct {\n\tquux int64\n\tsrv  *http.Server\n\tsi   someInt\n}\n\nfunc main() {\n\ttype foo string\n}\n\ntype Foo http.Client\n\nfunc CallsFoo(f Foo) {\n\n}\n\nfunc (f Foo) OnOtherToo(o other) {\n\n}\n\nfunc (o other) OnStruct() {\n\n}\nfunc (o other) OnStruct2() {\n\n}\n\nfunc (o *other) OnPtr() {\n\n}\nfunc (o *other) OnPtr2() {\n\n}\nfunc (o *other) OnPtr3() {\n\n}\n"
  },
  {
    "path": "testdata/inner.golden.out",
    "content": "# parameter 'f' at index 0 should be made into a pointer\nfunc CallsFoo(f Foo)\n\n# receiver, and parameter 'o' at index 0 should be made into pointers\nfunc (Foo).OnOtherToo(o other)\n\n# receiver should be made into a pointer\nfunc (other).OnStruct()\n\n# receiver should be made into a pointer\nfunc (other).OnStruct2()\n\n"
  }
]