Full Code of jmhodges/copyfighter for AI

master 90eb03bf42ab cached
7 files
11.8 KB
3.7k tokens
29 symbols
1 requests
Download .txt
Repository: jmhodges/copyfighter
Branch: master
Commit: 90eb03bf42ab
Files: 7
Total size: 11.8 KB

Directory structure:
gitextract_utfbp7ty/

├── .gitignore
├── .travis.yml
├── README.md
├── check_test.go
├── main.go
└── testdata/
    ├── inner.go
    └── inner.golden.out

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
copyfighter


================================================
FILE: .travis.yml
================================================
language: go
go:
  - 1.6.2


install:
  - go test -i .

script:
  - go test -v .


================================================
FILE: README.md
================================================
copyfighter
===========

Copyfighter statically analyzes Go code and reports functions that are passing
large structs by value. It helps you help your code.

Every Go function call copies the values given to it, including structs. When
large structs are passed around without using a pointer to them, the copying of
new data in memory causes more allocations and more work for your garbage
collector.

Copyfighter's static analysis will identify where large structs, without
pointers, are being used as method receivers, function parameters and return
values.

Install with `go get` or similar.

Example output
---------------
    $ copyfighter /gopath/path/to/pkg
    # parameter 'f' at index 0 should be made into a pointer
    func CallsFoo(f Foo)
    
    # receiver, and parameter 'o' at index 0 should be made into pointers
    func (Foo).OnOtherToo(o other)
    
    # receiver should be made into a pointer
    func (other).OnStruct()
    
    # receiver should be made into a pointer
    func (other).OnStruct2()

    $ copyfighter -max 32 path/to/pkg
    # parameter 'f' at index 0 should be made into a pointer
    func CallsFoo(f Foo)

    # receiver should be made into a pointer
    func (Foo).OnOtherToo(o other)

Defaults And Flags
------------------

By default, copyfighter assumes structs wider than 16 bytes (two words on x86\_64) should
not be copied. This can be adjusted with the `-max` flag. `max` should typically
be 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`.

Flags like `-max` have to go before the package name.

FAQ
---

Why not just use Go's heap profiler to fix these problems when they show up?

Because copyfighter can find problems before you put your code in production. It's nice to prevent issues before they matter.


================================================
FILE: check_test.go
================================================
package main

import (
	"bytes"
	"testing"
)

func TestGoldenPath(t *testing.T) {
	sites, fset, err := check("./testdata", 16, 8, 8)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	b := &bytes.Buffer{}
	printSites(sites, fset, b)
	actual := string(b.Bytes())
	if goldenData != actual {
		t.Errorf("output doesn't match, want:\n%s\n=============\ngot:\n%s", goldenData, actual)
	}
}

const goldenData = `testdata/inner.go:24:6: parameter 'f' at index 0 should be made into a pointer (func CallsFoo(f Foo))
testdata/inner.go:28:14: receiver, and parameter 'o' at index 0 should be made into pointers (func (Foo).OnOtherToo(o other))
testdata/inner.go:32:16: receiver should be made into a pointer (func (other).OnStruct())
testdata/inner.go:35:16: receiver should be made into a pointer (func (other).OnStruct2())
`


================================================
FILE: main.go
================================================
package main

import (
	"flag"
	"fmt"
	"go/ast"
	"go/build"
	"go/parser"
	"go/token"
	"go/types"
	"io"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"golang.org/x/tools/go/gcexportdata"
)

var (
	maxStructWidth = flag.Int64("max", 16, "maximum size in bytes a struct can be before by-value uses are flagged")
	wordSize       = flag.Int64("wordSize", 8, "word size to assume when calculation struct size")
	maxAlign       = flag.Int64("maxAlign", 8, "maximum word alignment to assume when calculating struct size")
)

func main() {
	log.SetPrefix("")
	log.SetFlags(0)
	flag.Parse()

	if flag.NArg() != 1 {
		log.Fatalf("usage: %s GO_PKG_DIR", os.Args[0])
	}
	p := flag.Arg(0)
	sites, fset, err := check(p, *maxStructWidth, *wordSize, *maxAlign)
	if err != nil {
		log.Fatal(err)
	}
	printSites(sites, fset, os.Stdout)
	if len(sites) > 0 {
		os.Exit(2)
	}

}

func check(p string, maxStructWidth, wordSize, maxAlign int64) ([]copySite, *token.FileSet, error) {
	fset := token.NewFileSet()

	_, err := os.Stat(p)
	switch {
	case os.IsNotExist(err):
		// File doesn't exist, probably a Go import path
		pkgs, err := parseGoPkg(p, fset)
		if err != nil {
			return nil, nil, err
		}
		sites := []copySite{}
		for _, pkg := range pkgs {
			s, err := checkPkg(pkg, fset, maxStructWidth, wordSize, maxAlign)
			if err != nil {
				return nil, nil, err
			}
			sites = append(sites, s...)
		}
		return sites, fset, nil
	case err == nil:
		// File exists, parses as such
		pkg, err := parsePkgDir(p, fset)
		if err != nil {
			return nil, nil, err
		}
		sites, err := checkPkg(pkg, fset, maxStructWidth, wordSize, maxAlign)
		if err != nil {
			return nil, nil, err
		}
		return sites, fset, nil
	default:
		return nil, nil, err
	}
}

func parsePkgDir(p string, fset *token.FileSet) (*ast.Package, error) {
	fi, err := os.Stat(p)
	if err != nil {
		return nil, fmt.Errorf("unable to stat file %#v: %s", p, err)
	}
	if !fi.IsDir() {
		return nil, fmt.Errorf("%#v is not a directory", p)
	}

	buildContext := build.Default
	bpkg, err := buildContext.ImportDir(p, 0)
	if err != nil {
		return nil, fmt.Errorf("unable to parse package at %#v: %s", p, err)
	}

	mp, err := parser.ParseDir(fset, p, func(i os.FileInfo) bool {
		for _, f := range bpkg.IgnoredGoFiles {
			if f == i.Name() {
				return false
			}
		}
		for _, f := range bpkg.InvalidGoFiles {
			if f == i.Name() {
				return false
			}
		}
		return true
	}, 0)
	if err != nil {
		return nil, fmt.Errorf("unable to parse package at %#v: %s", p, err)
	}
	if len(mp) != 1 {
		var ps []string
		for _, pkg := range mp {
			ps = append(ps, pkg.Name)
		}
		return nil, fmt.Errorf("more than one package found in %#v: %s", p, strings.Join(ps, ","))
	}
	var pkg *ast.Package
	for _, v := range mp {
		pkg = v
	}
	return pkg, nil
}

func pathToRegexp(p string) *regexp.Regexp {
	re := regexp.QuoteMeta(p)
	re = strings.Replace(re, `\.\.\.`, `.*`, -1)
	// Special case: foo/... matches foo too.
	if strings.HasSuffix(re, `/.*`) {
		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
	}
	return regexp.MustCompile(`^` + re + `$`)
}

func parseGoPkg(p string, fset *token.FileSet) ([]*ast.Package, error) {
	p = filepath.Clean(p)
	dirs := []string{}
	re := pathToRegexp(p)
	buildContext := build.Default
	for _, src := range buildContext.SrcDirs() {
		src = filepath.Clean(src) + string(filepath.Separator)
		root := src
		filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
			if err != nil || !fi.IsDir() || path == src {
				return nil
			}

			// Avoid .foo, _foo, and testdata directory trees.
			_, elem := filepath.Split(path)
			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
				return filepath.SkipDir
			}
			name := filepath.ToSlash(path[len(src):])
			if re.MatchString(name) {
				dirs = append(dirs, path)
			}
			return nil
		})
	}

	pkgs := []*ast.Package{}
	for _, d := range dirs {
		_, err := buildContext.ImportDir(d, 0)
		if err != nil {
			if _, noGo := err.(*build.NoGoError); noGo {
				continue
			}
			return nil, fmt.Errorf("unable to build code in %#v: %s", d, err)
		}
		pkg, err := parsePkgDir(d, fset)
		if err != nil {
			return nil, err
		}
		pkgs = append(pkgs, pkg)
	}
	if len(pkgs) == 0 {
		return nil, fmt.Errorf("unable to find packages matching %#v", p)
	}

	return pkgs, nil
}

func checkPkg(pkg *ast.Package, fset *token.FileSet, maxWidth, wordSize, maxAlign int64) ([]copySite, error) {
	sizes := &types.StdSizes{WordSize: wordSize, MaxAlign: maxAlign}
	info := &types.Info{
		// Types is required to prevent duplicates, it seems, in Defs.
		Types: make(map[ast.Expr]types.TypeAndValue),
		Defs:  make(map[*ast.Ident]types.Object),
	}

	conf := &types.Config{
		Importer:                 gcexportdata.NewImporter(fset, make(map[string]*types.Package)),
		DisableUnusedImportCheck: true,
		Sizes:                    sizes,
	}
	files := []*ast.File{}
	for _, f := range pkg.Files {
		files = append(files, f)
	}

	_, err := conf.Check("", fset, files, info)
	if err != nil {
		return nil, fmt.Errorf("unable to type check package %#v: %s", pkg.Name, err)
	}

	wideStructs := make(map[string]bool)

	funcs := []*types.Func{}
	for _, obj := range info.Defs {
		if tn, ok := obj.(*types.TypeName); ok {
			if sizes.Sizeof(tn.Type()) > maxWidth {
				wideStructs[tn.Id()] = true
			}
		}
		if f, ok := obj.(*types.Func); ok {
			funcs = append(funcs, f)
		}
	}

	sites := findCopySites(funcs, wideStructs)

	return sites, nil
}

// findCopySites returns a slice of copySites that represent Go function calls
// that use a large struct without a pointer to it. The wideStructs argument is
// a map of the struct's TypeName id to its TypeName object.
func findCopySites(funcs []*types.Func, wideStructs map[string]bool) []copySite {
	sites := []copySite{}
	for _, f := range funcs {
		s := f.Type().(*types.Signature)
		shouldBe := []string{}

		// If the func is a method, check the receiver
		if s.Recv() != nil {
			rt := s.Recv().Type()
			if isWideStructTyped(rt, wideStructs) {
				shouldBe = append(shouldBe, "receiver")
			}
		}

		params := s.Params()
		for i := 0; i < params.Len(); i++ {
			v := params.At(i)
			if isWideStructTyped(v.Type(), wideStructs) {
				name := v.Name()
				parameter := "parameter"
				if name != "" {
					parameter = fmt.Sprintf("parameter '%s'", name)
				}
				shouldBe = append(shouldBe,
					fmt.Sprintf("%s at index %d", parameter, i))
			}
		}

		results := s.Results()
		for i := 0; i < results.Len(); i++ {
			v := results.At(i)
			if isWideStructTyped(v.Type(), wideStructs) {
				shouldBe = append(shouldBe,
					fmt.Sprintf("return value '%s' at index %d", v.Type(), i))
			}
		}
		if len(shouldBe) > 0 {
			sites = append(sites, copySite{f, shouldBe})
		}
	}
	return sites
}

func printSites(sites []copySite, fset *token.FileSet, w io.Writer) {
	sort.Sort(sortedCopySites{sites: sites, fset: fset})
	for _, site := range sites {
		f := site.fun
		shouldBe := site.shouldBe
		sb := sentence(shouldBe)
		msg := "should be made into"
		if len(shouldBe) > 1 {
			msg += " pointers"
		} else {
			msg += " a pointer"
		}
		pos := site.fun.Pos()
		file := fset.File(pos)
		position := file.Position(pos)
		fmt.Fprintf(w, "%s:%d:%d: %s %s (%s)\n", file.Name(), position.Line, position.Column, sb, msg, f)
	}
}

type copySite struct {
	fun      *types.Func
	shouldBe []string
}

// sortedCopySites sorts copySites as ordered by the filename, line, and column
// the func was found at.
type sortedCopySites struct {
	sites []copySite
	fset  *token.FileSet
}

func (s sortedCopySites) Len() int {
	return len(s.sites)
}
func (s sortedCopySites) Swap(i, j int) {
	s.sites[i], s.sites[j] = s.sites[j], s.sites[i]
}

func (s sortedCopySites) Less(i, j int) bool {
	left := s.fset.Position(s.sites[i].fun.Pos())
	right := s.fset.Position(s.sites[j].fun.Pos())

	if left.Filename != right.Filename {
		return left.Filename < right.Filename
	}
	if left.Line != right.Line {
		return left.Line < right.Line
	}
	return left.Column < right.Column
}

// isWideStructTyped returns true if the given type is a struct (not a pointer to
// a struct) that is in wideStructs.
func isWideStructTyped(t types.Type, wideStructs map[string]bool) bool {
	if named, ok := t.(*types.Named); ok {
		return wideStructs[named.Obj().Id()]
	}
	return false
}

func sentence(parts []string) string {
	if len(parts) == 0 {
		return ""
	}
	if len(parts) == 1 {
		return parts[0]
	}
	last := len(parts) - 1
	return strings.Join(parts[:last], ", ") + ", and " + parts[last]
}


================================================
FILE: testdata/inner.go
================================================
package main

import "net/http"

type someInt interface {
	Bang()
}
type bar struct {
	baz int
}

type other struct {
	quux int64
	srv  *http.Server
	si   someInt
}

func main() {
	type foo string
}

type Foo http.Client

func CallsFoo(f Foo) {

}

func (f Foo) OnOtherToo(o other) {

}

func (o other) OnStruct() {

}
func (o other) OnStruct2() {

}

func (o *other) OnPtr() {

}
func (o *other) OnPtr2() {

}
func (o *other) OnPtr3() {

}


================================================
FILE: testdata/inner.golden.out
================================================
# parameter 'f' at index 0 should be made into a pointer
func CallsFoo(f Foo)

# receiver, and parameter 'o' at index 0 should be made into pointers
func (Foo).OnOtherToo(o other)

# receiver should be made into a pointer
func (other).OnStruct()

# receiver should be made into a pointer
func (other).OnStruct2()

Download .txt
gitextract_utfbp7ty/

├── .gitignore
├── .travis.yml
├── README.md
├── check_test.go
├── main.go
└── testdata/
    ├── inner.go
    └── inner.golden.out
Download .txt
SYMBOL INDEX (29 symbols across 3 files)

FILE: check_test.go
  function TestGoldenPath (line 8) | func TestGoldenPath(t *testing.T) {
  constant goldenData (line 21) | goldenData = `testdata/inner.go:24:6: parameter 'f' at index 0 should be...

FILE: main.go
  function main (line 28) | func main() {
  function check (line 48) | func check(p string, maxStructWidth, wordSize, maxAlign int64) ([]copySi...
  function parsePkgDir (line 84) | func parsePkgDir(p string, fset *token.FileSet) (*ast.Package, error) {
  function pathToRegexp (line 129) | func pathToRegexp(p string) *regexp.Regexp {
  function parseGoPkg (line 139) | func parseGoPkg(p string, fset *token.FileSet) ([]*ast.Package, error) {
  function checkPkg (line 187) | func checkPkg(pkg *ast.Package, fset *token.FileSet, maxWidth, wordSize,...
  function findCopySites (line 232) | func findCopySites(funcs []*types.Func, wideStructs map[string]bool) []c...
  function printSites (line 275) | func printSites(sites []copySite, fset *token.FileSet, w io.Writer) {
  type copySite (line 294) | type copySite struct
  type sortedCopySites (line 301) | type sortedCopySites struct
    method Len (line 306) | func (s sortedCopySites) Len() int {
    method Swap (line 309) | func (s sortedCopySites) Swap(i, j int) {
    method Less (line 313) | func (s sortedCopySites) Less(i, j int) bool {
  function isWideStructTyped (line 328) | func isWideStructTyped(t types.Type, wideStructs map[string]bool) bool {
  function sentence (line 335) | func sentence(parts []string) string {

FILE: testdata/inner.go
  type someInt (line 5) | type someInt interface
  type bar (line 8) | type bar struct
  type other (line 12) | type other struct
    method OnStruct (line 32) | func (o other) OnStruct() {
    method OnStruct2 (line 35) | func (o other) OnStruct2() {
    method OnPtr (line 39) | func (o *other) OnPtr() {
    method OnPtr2 (line 42) | func (o *other) OnPtr2() {
    method OnPtr3 (line 45) | func (o *other) OnPtr3() {
  function main (line 18) | func main() {
  type Foo (line 22) | type Foo
    method OnOtherToo (line 28) | func (f Foo) OnOtherToo(o other) {
  function CallsFoo (line 24) | func CallsFoo(f Foo) {
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (13K chars).
[
  {
    "path": ".gitignore",
    "chars": 12,
    "preview": "copyfighter\n"
  },
  {
    "path": ".travis.yml",
    "chars": 81,
    "preview": "language: go\ngo:\n  - 1.6.2\n\n\ninstall:\n  - go test -i .\n\nscript:\n  - go test -v .\n"
  },
  {
    "path": "README.md",
    "chars": 1871,
    "preview": "copyfighter\n===========\n\nCopyfighter statically analyzes Go code and reports functions that are passing\nlarge structs by"
  },
  {
    "path": "check_test.go",
    "chars": 827,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestGoldenPath(t *testing.T) {\n\tsites, fset, err := check(\"./testdata"
  },
  {
    "path": "main.go",
    "chars": 8524,
    "preview": "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\"pa"
  },
  {
    "path": "testdata/inner.go",
    "chars": 441,
    "preview": "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\tq"
  },
  {
    "path": "testdata/inner.golden.out",
    "chars": 314,
    "preview": "# parameter 'f' at index 0 should be made into a pointer\nfunc CallsFoo(f Foo)\n\n# receiver, and parameter 'o' at index 0 "
  }
]

About this extraction

This page contains the full source code of the jmhodges/copyfighter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (11.8 KB), approximately 3.7k tokens, and a symbol index with 29 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!