diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e2a398..49aacbd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,10 +3,10 @@ version: 2 jobs: build: docker: - - image: circleci/golang:1.12 + - image: cimg/go:1.17.7 steps: - checkout - - run: sudo apt-get -y -qq install python python-pip + - run: sudo apt-get update && sudo apt-get -y -qq install python pip - run: pip install pre-commit - run: SKIP=no-commit-to-branch pre-commit run -a - run: go test ./... diff --git a/.golangci.yml b/.golangci.yml index 8ba4236..2002904 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,2 +1 @@ -linters: - enable-all: true +linters: {} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3586c7c..eeb9d98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,9 @@ repos: - repo: https://github.com/golangci/golangci-lint - rev: v1.17.1 + rev: v1.44.2 hooks: - id: golangci-lint + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace \ No newline at end of file diff --git a/README.md b/README.md index 36c2819..0112760 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # makezero +[![CircleCI](https://circleci.com/gh/ashanbrown/makezero/tree/master.svg?style=svg)](https://circleci.com/gh/ashanbrown/makezero/tree/master) + makezero is a Go static analysis tool to find slice declarations that are not initialized with zero length and are later used with append. diff --git a/go.mod b/go.mod index b47b2d1..a540cde 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.12 require ( github.com/stretchr/testify v1.4.0 - golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc + golang.org/x/tools v0.1.9 ) diff --git a/go.sum b/go.sum index 44a91e0..5b260ae 100644 --- a/go.sum +++ b/go.sum @@ -2,24 +2,37 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc h1:+GB9/q0gCzmtaIl6WdoJFMS3lPwrR6rpcMyY6jfQHAw= -golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/main.go b/main.go index 2f7e656..bcde6ad 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ func main() { } linter := makezero.NewLinter(*always) - var issues []makezero.Issue // nolint:prealloc // don't know how many there will be + var issues []makezero.Issue for _, p := range pkgs { nodes := make([]ast.Node, 0, len(p.Syntax)) for _, n := range p.Syntax { diff --git a/makezero/makezero.go b/makezero/makezero.go index 89cfcf4..18bcad3 100644 --- a/makezero/makezero.go +++ b/makezero/makezero.go @@ -1,4 +1,4 @@ -// makezero provides a linter for appends to slices initialized with non-zero length. +// Package makezero provides a linter for appends to slices initialized with non-zero length. package makezero import ( @@ -13,7 +13,7 @@ import ( ) // a decl might include multiple var, -// so var name with decl make final uniq obj +// so var name with decl make final uniq obj. type uniqDecl struct { varName string decl interface{} @@ -21,12 +21,14 @@ type uniqDecl struct { type Issue interface { Details() string + Pos() token.Pos Position() token.Position String() string } type AppendIssue struct { name string + pos token.Pos position token.Position } @@ -34,6 +36,10 @@ func (a AppendIssue) Details() string { return fmt.Sprintf("append to slice `%s` with non-zero initialized length", a.name) } +func (a AppendIssue) Pos() token.Pos { + return a.pos +} + func (a AppendIssue) Position() token.Position { return a.position } @@ -42,6 +48,7 @@ func (a AppendIssue) String() string { return toString(a) } type MustHaveNonZeroInitLenIssue struct { name string + pos token.Pos position token.Position } @@ -49,6 +56,10 @@ func (i MustHaveNonZeroInitLenIssue) Details() string { return fmt.Sprintf("slice `%s` does not have non-zero initial length", i.name) } +func (i MustHaveNonZeroInitLenIssue) Pos() token.Pos { + return i.pos +} + func (i MustHaveNonZeroInitLenIssue) Position() token.Position { return i.position } @@ -81,7 +92,7 @@ func NewLinter(initialLengthMustBeZero bool) *Linter { } func (l Linter) Run(fset *token.FileSet, info *types.Info, nodes ...ast.Node) ([]Issue, error) { - var issues []Issue // nolint:prealloc // don't know how many there will be + var issues []Issue for _, node := range nodes { var comments []*ast.CommentGroup if file, ok := node.(*ast.File); ok { @@ -110,7 +121,12 @@ func (v *visitor) Visit(node ast.Node) ast.Visitor { if sliceIdent, ok := node.Args[0].(*ast.Ident); ok && v.hasNonZeroInitialLength(sliceIdent) && !v.hasNoLintOnSameLine(fun) { - v.issues = append(v.issues, AppendIssue{name: sliceIdent.Name, position: v.fset.Position(fun.Pos())}) + v.issues = append(v.issues, + AppendIssue{ + name: sliceIdent.Name, + pos: fun.Pos(), + position: v.fset.Position(fun.Pos()), + }) } case *ast.AssignStmt: for i, right := range node.Rhs { @@ -130,6 +146,7 @@ func (v *visitor) Visit(node ast.Node) ast.Visitor { if v.initLenMustBeZero && !v.hasNoLintOnSameLine(fun) { v.issues = append(v.issues, MustHaveNonZeroInitLenIssue{ name: v.textFor(left), + pos: node.Pos(), position: v.fset.Position(node.Pos()), }) } @@ -201,7 +218,7 @@ func (v *visitor) isSlice(node ast.Node) bool { } func (v *visitor) hasNoLintOnSameLine(node ast.Node) bool { - var nolint = regexp.MustCompile(`^\s*nozero\b`) + nolint := regexp.MustCompile(`^\s*nozero\b`) nodePos := v.fset.Position(node.Pos()) for _, c := range v.comments { commentPos := v.fset.Position(c.Pos()) diff --git a/makezero/makezero_test.go b/makezero/makezero_test.go index 22e0d18..7b016ae 100644 --- a/makezero/makezero_test.go +++ b/makezero/makezero_test.go @@ -64,7 +64,6 @@ func foo() { append(x, 1) //nozeroxxx }`, "append to slice `x` with non-zero initialized length at testing.go:7:3") }) - }) t.Run("ignores more complex constructs than basic variables", func(t *testing.T) { diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go new file mode 100644 index 0000000..30c0b83 --- /dev/null +++ b/pkg/analyzer/analyzer.go @@ -0,0 +1,52 @@ +package analyzer + +import ( + "flag" + "go/ast" + + "github.com/ashanbrown/makezero/makezero" + "golang.org/x/tools/go/analysis" +) + +type analyzer struct { + always bool +} + +// NewAnalyzer returns a go/analysis-compatible analyzer +// Set "-always" to report any non-empty slice initialization. +func NewAnalyzer() *analysis.Analyzer { + var flags flag.FlagSet + a := analyzer{} + flags.BoolVar(&a.always, "always", false, "report any non-empty slice initializations, regardless of intention") + return &analysis.Analyzer{ + Name: "makezero", + Doc: "detect unintended non-empty slice initializations", + Run: a.runAnalysis, + Flags: flags, + } +} + +func (a *analyzer) runAnalysis(pass *analysis.Pass) (interface{}, error) { + linter := makezero.NewLinter(a.always) + nodes := make([]ast.Node, 0, len(pass.Files)) + for _, f := range pass.Files { + nodes = append(nodes, f) + } + issues, err := linter.Run(pass.Fset, pass.TypesInfo, nodes...) + if err != nil { + return nil, err + } + reportIssues(pass, issues) + return nil, nil +} + +func reportIssues(pass *analysis.Pass, issues []makezero.Issue) { + for _, i := range issues { + diag := analysis.Diagnostic{ + Pos: i.Pos(), + Message: i.Details(), + Category: "restriction", + } + pass.Report(diag) + } +} diff --git a/pkg/analyzer/analyzer_test.go b/pkg/analyzer/analyzer_test.go new file mode 100644 index 0000000..5eaa7bb --- /dev/null +++ b/pkg/analyzer/analyzer_test.go @@ -0,0 +1,24 @@ +package analyzer_test + +import ( + "testing" + + "github.com/ashanbrown/makezero/pkg/analyzer" + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAppend(t *testing.T) { + testdata := analysistest.TestData() + a := analyzer.NewAnalyzer() + analysistest.Run(t, testdata, a, "./append") +} + +func TestAlways(t *testing.T) { + testdata := analysistest.TestData() + a := analyzer.NewAnalyzer() + err := a.Flags.Set("always", "true") + if err != nil { + t.Fatalf("expected no error but got %q", err) + } + analysistest.Run(t, testdata, a, "./always") +} diff --git a/pkg/analyzer/testdata/always/append.go b/pkg/analyzer/testdata/always/append.go new file mode 100644 index 0000000..a0ecc6b --- /dev/null +++ b/pkg/analyzer/testdata/always/append.go @@ -0,0 +1,8 @@ +package always + +import "fmt" + +func Foo() { + x := make([]int, 5) // want "slice `x` does not have non-zero initial length" + fmt.Println(x) +} diff --git a/pkg/analyzer/testdata/append/append.go b/pkg/analyzer/testdata/append/append.go new file mode 100644 index 0000000..1437742 --- /dev/null +++ b/pkg/analyzer/testdata/append/append.go @@ -0,0 +1,9 @@ +package append + +import "fmt" + +func Foo() { + x := make([]int, 5) + x = append(x, 1) // want "append to slice `x` with non-zero initialized length" + fmt.Println(x) +}