diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba78129 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gochecknoglobals diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1fec735 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - "1.x" + +go_import_path: 4d63.com/gochecknoglobals + +script: + - go test -coverprofile=coverage.txt + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c401e66 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Leigh McCulloch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5811499 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# gochecknoglobals + +[![Build Status](https://img.shields.io/travis/leighmcculloch/gochecknoglobals.svg)](https://travis-ci.org/leighmcculloch/gochecknoglobals) +[![Codecov](https://img.shields.io/codecov/c/github/leighmcculloch/gochecknoglobals.svg)](https://codecov.io/gh/leighmcculloch/gochecknoglobals) +[![Go Report Card](https://goreportcard.com/badge/github.com/leighmcculloch/gochecknoglobals)](https://goreportcard.com/report/github.com/leighmcculloch/gochecknoglobals) + +Check that no globals are present in Go code. + +## Why + +Global variables are an input to functions that is not visible in the functions signature, complicate testing, reduces readability and increase the complexity of code. + +https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html +https://twitter.com/davecheney/status/871939730761547776 + +## Install + +``` +go get 4d63.com/gochecknoglobals +``` + +## Usage + +``` +gochecknoglobals +``` + +or + +``` +gochecknoglobals [path] [path] ... +``` diff --git a/check_no_globals.go b/check_no_globals.go new file mode 100644 index 0000000..e38103f --- /dev/null +++ b/check_no_globals.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" +) + +func checkNoGlobals(path string) ([]string, error) { + messages := []string{} + + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, path, nil, 0) + if err != nil { + return err + } + + for _, pkg := range pkgs { + for _, file := range pkg.Files { + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + if genDecl.Tok != token.VAR { + continue + } + filename := fset.Position(genDecl.TokPos).Filename + line := fset.Position(genDecl.TokPos).Line + valueSpec := genDecl.Specs[0].(*ast.ValueSpec) + for i := 0; i < len(valueSpec.Names); i++ { + name := valueSpec.Names[i].Name + message := fmt.Sprintf("%s:%d %s is a global variable", filename, line, name) + messages = append(messages, message) + } + } + } + } + return nil + }) + + return messages, err +} diff --git a/check_no_globals_test.go b/check_no_globals_test.go new file mode 100644 index 0000000..bb3a5da --- /dev/null +++ b/check_no_globals_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "path/filepath" + "strconv" + "testing" +) + +func TestCheckNoGlobals(t *testing.T) { + cases := [][]string{ + nil, + nil, + { + "testdata/2/code.go:3 myVar is a global variable", + }, + { + "testdata/3/code_0.go:8 theVar is a global variable", + "testdata/3/code_1.go:3 myVar is a global variable", + }, + { + "testdata/4/subpkg/code_1.go:3 myVar is a global variable", + }, + { + "testdata/5/code.go:3 myVar1 is a global variable", + "testdata/5/code.go:3 myVar2 is a global variable", + }, + } + + for i, wantMessages := range cases { + testdataName := strconv.FormatInt(int64(i), 10) + t.Run(testdataName, func(t *testing.T) { + path := filepath.Join("testdata", testdataName) + messages, err := checkNoGlobals(path) + if err != nil { + t.Fatalf("got error %#v", err) + } + if !stringSlicesEqual(messages, wantMessages) { + t.Errorf("got %#v, want %#v", messages, wantMessages) + } + }) + } +} + +func stringSlicesEqual(s1, s2 []string) bool { + diff := map[string]int{} + for _, s := range s1 { + diff[s]++ + } + for _, s := range s2 { + diff[s]-- + if diff[s] == 0 { + delete(diff, s) + } + } + return len(diff) == 0 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..6a03223 --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +func main() { + flagPrintHelp := flag.Bool("help", false, "") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: gochecknoglobals [path] [path] ...\n") + } + flag.Parse() + + if *flagPrintHelp { + flag.Usage() + return + } + + paths := flag.Args() + if len(paths) == 0 { + paths = []string{"."} + } + + for _, path := range paths { + messages, err := checkNoGlobals(path) + for _, message := range messages { + fmt.Fprintf(os.Stdout, "%s\n", message) + } + if err != nil { + fmt.Fprintf(os.Stderr, "error: %s\n", err) + } + } +} diff --git a/testdata/0/code.go b/testdata/0/code.go new file mode 100644 index 0000000..e85e253 --- /dev/null +++ b/testdata/0/code.go @@ -0,0 +1 @@ +package code diff --git a/testdata/1/code.go b/testdata/1/code.go new file mode 100644 index 0000000..52d61b1 --- /dev/null +++ b/testdata/1/code.go @@ -0,0 +1,3 @@ +package code + +const constant = 0 diff --git a/testdata/2/code.go b/testdata/2/code.go new file mode 100644 index 0000000..e2adbde --- /dev/null +++ b/testdata/2/code.go @@ -0,0 +1,3 @@ +package code + +var myVar = 0 diff --git a/testdata/3/code_0.go b/testdata/3/code_0.go new file mode 100644 index 0000000..0a11808 --- /dev/null +++ b/testdata/3/code_0.go @@ -0,0 +1,8 @@ +package code + +func someCode() bool { + yourVar := true + return yourVar +} + +var theVar = true diff --git a/testdata/3/code_1.go b/testdata/3/code_1.go new file mode 100644 index 0000000..9c14608 --- /dev/null +++ b/testdata/3/code_1.go @@ -0,0 +1,3 @@ +package code + +var myVar = "global" diff --git a/testdata/4/subpkg/code_0.go b/testdata/4/subpkg/code_0.go new file mode 100644 index 0000000..d6f0baa --- /dev/null +++ b/testdata/4/subpkg/code_0.go @@ -0,0 +1,5 @@ +package code + +func someCode() bool { + return true +} diff --git a/testdata/4/subpkg/code_1.go b/testdata/4/subpkg/code_1.go new file mode 100644 index 0000000..9c14608 --- /dev/null +++ b/testdata/4/subpkg/code_1.go @@ -0,0 +1,3 @@ +package code + +var myVar = "global" diff --git a/testdata/5/code.go b/testdata/5/code.go new file mode 100644 index 0000000..c47adf0 --- /dev/null +++ b/testdata/5/code.go @@ -0,0 +1,3 @@ +package code + +var myVar1, myVar2 = 1, 2