├── testdata └── src │ ├── e │ └── e.go │ └── s │ └── s.go ├── go.mod ├── cmd └── exhaustivestruct │ └── main.go ├── .gitignore ├── pkg └── analyzer │ ├── analyzer_test.go │ └── analyzer.go ├── .github └── workflows │ └── go.yml ├── README.md ├── LICENSE └── go.sum /testdata/src/e/e.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | type External struct { 4 | A string 5 | B string 6 | c string 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mbilski/exhaustivestruct 2 | 3 | go 1.15 4 | 5 | require golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c 6 | -------------------------------------------------------------------------------- /cmd/exhaustivestruct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/mbilski/exhaustivestruct/pkg/analyzer" 7 | "golang.org/x/tools/go/analysis/singlechecker" 8 | ) 9 | 10 | func main() { 11 | flag.Bool("unsafeptr", false, "") 12 | singlechecker.Main(analyzer.Analyzer) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package analyzer_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/mbilski/exhaustivestruct/pkg/analyzer" 9 | "golang.org/x/tools/go/analysis/analysistest" 10 | ) 11 | 12 | func TestAll(t *testing.T) { 13 | wd, err := os.Getwd() 14 | if err != nil { 15 | t.Fatalf("Failed to get wd: %s", err) 16 | } 17 | 18 | testdata := filepath.Join(filepath.Dir(filepath.Dir(wd)), "testdata") 19 | analyzer.StructPatternList = "*.Test,*.Test2,*.Embedded,*.External" 20 | analysistest.Run(t, testdata, analyzer.Analyzer, "s") 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | 29 | - name: Build 30 | run: go build ./... 31 | 32 | - name: Test 33 | run: go test ./... 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exhaustivestruct 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/mbilski/exhaustivestruct)](https://goreportcard.com/badge/github.com/mbilski/exhaustivestruct) 4 | 5 | exhaustivestruct is a go static analysis tool to find structs that have uninitialized fields. 6 | 7 | > :warning: This linter is meant to be used only for special cases. 8 | > It is not recommended to use it for all files in a project. 9 | 10 | ## Installation 11 | 12 | ``` 13 | go get -u github.com/mbilski/exhaustivestruct/cmd/exhaustivestruct 14 | ``` 15 | 16 | ## Usage 17 | 18 | ``` 19 | Usage: exhaustivestruct [-flag] [package] 20 | 21 | Flags: 22 | -struct_patterns string 23 | This is a comma separated list of expressions to match struct packages and names 24 | ``` 25 | 26 | ## Example 27 | 28 | ``` go 29 | type User struct { 30 | Name string 31 | Age int 32 | } 33 | 34 | var user = User{ // fails with "Age is missing in User" 35 | Name: "John", 36 | } 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mateusz Bilski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 4 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 5 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 6 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 7 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 8 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 9 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 10 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 11 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 12 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 18 | golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c h1:9BSeO6440XJVa2mxIcRAndAol4g4g2KflCVGcHx9Yu8= 19 | golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 20 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 21 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 22 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 23 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | -------------------------------------------------------------------------------- /testdata/src/s/s.go: -------------------------------------------------------------------------------- 1 | package s 2 | 3 | import ( 4 | "e" 5 | "fmt" 6 | ) 7 | 8 | type Embedded struct { 9 | E string 10 | F string 11 | g string 12 | H string 13 | } 14 | 15 | type Test struct { 16 | A string 17 | B int 18 | C float32 19 | D bool 20 | } 21 | 22 | type Test2 struct { 23 | Embedded 24 | External e.External 25 | } 26 | 27 | func shouldPass() Test { 28 | return Test{ 29 | A: "a", 30 | B: 1, 31 | C: 0.0, 32 | D: false, 33 | } 34 | } 35 | 36 | func shouldPass2() Test2 { 37 | return Test2{ 38 | External: e.External{ 39 | A: "", 40 | B: "", 41 | }, 42 | Embedded: Embedded{ 43 | E: "", 44 | F: "", 45 | H: "", 46 | g: "", 47 | }, 48 | } 49 | } 50 | 51 | func shouldPassWithReturn() (Test, error) { 52 | // Empty structs in return statements are ignored if also returning an error 53 | return Test{}, fmt.Errorf("error") 54 | } 55 | func shouldPass3() { 56 | // Checking to make sure state from tracking the previous return statement doesn't affect this struct 57 | _ = Test{} // want "A, B, C, D are missing in Test" 58 | } 59 | 60 | func shouldPassWithoutNames() Test { 61 | return Test{"", 0, 0, false} 62 | } 63 | 64 | func shouldFailWithReturn() (Test, error) { 65 | // Empty structs in return statements are not ignored if returning nil error 66 | return Test{}, nil // want "A, B, C, D are missing in Test" 67 | } 68 | 69 | func shouldFailWithMissingFields() Test { 70 | return Test{ // want "C is missing in Test" 71 | A: "a", 72 | B: 1, 73 | D: false, 74 | } 75 | } 76 | 77 | // Unchecked is a struct not listed in StructPatternList 78 | type Unchecked struct { 79 | A int 80 | B int 81 | } 82 | 83 | func unchecked() { 84 | // This struct is not listed in StructPatternList so the linter won't complain that it's not filled out 85 | _ = Unchecked{ 86 | A: 1, 87 | } 88 | } 89 | 90 | func shouldFailOnEmbedded() Test2 { 91 | return Test2{ 92 | Embedded: Embedded{ // want "E, g, H are missing in Embedded" 93 | F: "", 94 | }, 95 | External: e.External{ 96 | A: "", 97 | B: "", 98 | }, 99 | } 100 | } 101 | 102 | func shoildFailOnExternal() Test2 { 103 | return Test2{ 104 | External: e.External{ // want "A is missing in External" 105 | B: "", 106 | }, 107 | Embedded: Embedded{ 108 | E: "", 109 | F: "", 110 | H: "", 111 | g: "", 112 | }, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzer.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/ast" 7 | "go/types" 8 | "path" 9 | "strings" 10 | 11 | "golang.org/x/tools/go/analysis/passes/inspect" 12 | "golang.org/x/tools/go/ast/inspector" 13 | 14 | "golang.org/x/tools/go/analysis" 15 | ) 16 | 17 | // Analyzer that checks if all struct's fields are initialized 18 | var Analyzer = &analysis.Analyzer{ 19 | Name: "exhaustivestruct", 20 | Doc: "Checks if all struct's fields are initialized", 21 | Run: run, 22 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 23 | Flags: newFlagSet(), 24 | } 25 | 26 | // StructPatternList is a comma separated list of expressions to match struct packages and names 27 | // The struct packages have the form example.com/package.ExampleStruct 28 | // The matching patterns can use matching syntax from https://pkg.go.dev/path#Match 29 | // If this list is empty, all structs are tested. 30 | var StructPatternList string 31 | 32 | func newFlagSet() flag.FlagSet { 33 | fs := flag.NewFlagSet("", flag.PanicOnError) 34 | fs.StringVar(&StructPatternList, "struct_patterns", "", "This is a comma separated list of expressions to match struct packages and names") 35 | return *fs 36 | } 37 | 38 | func run(pass *analysis.Pass) (interface{}, error) { 39 | splitFn := func(c rune) bool { return c == ',' } 40 | inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 41 | structPatterns := strings.FieldsFunc(StructPatternList, splitFn) 42 | // validate the pattern syntax 43 | for _, pattern := range structPatterns { 44 | _, err := path.Match(pattern, "") 45 | if err != nil { 46 | return nil, fmt.Errorf("invalid struct pattern %s: %w", pattern, err) 47 | } 48 | } 49 | 50 | nodeFilter := []ast.Node{ 51 | (*ast.CompositeLit)(nil), 52 | (*ast.ReturnStmt)(nil), 53 | } 54 | 55 | var returnStmt *ast.ReturnStmt 56 | 57 | inspector.Preorder(nodeFilter, func(node ast.Node) { 58 | var name string 59 | 60 | compositeLit, ok := node.(*ast.CompositeLit) 61 | if !ok { 62 | // Keep track of the last return statement whilte iterating 63 | retLit, ok := node.(*ast.ReturnStmt) 64 | if ok { 65 | returnStmt = retLit 66 | } 67 | return 68 | } 69 | 70 | i, ok := compositeLit.Type.(*ast.Ident) 71 | 72 | if ok { 73 | name = i.Name 74 | } else { 75 | s, ok := compositeLit.Type.(*ast.SelectorExpr) 76 | 77 | if !ok { 78 | return 79 | } 80 | 81 | name = s.Sel.Name 82 | } 83 | 84 | if compositeLit.Type == nil { 85 | return 86 | } 87 | 88 | t := pass.TypesInfo.TypeOf(compositeLit.Type) 89 | 90 | if t == nil { 91 | return 92 | } 93 | 94 | if len(structPatterns) > 0 { 95 | shouldLint := false 96 | for _, pattern := range structPatterns { 97 | // We check the patterns for vailidy ahead of time, so we don't need to check the error here 98 | if match, _ := path.Match(pattern, t.String()); match { 99 | shouldLint = true 100 | break 101 | } 102 | } 103 | if !shouldLint { 104 | return 105 | } 106 | } 107 | 108 | str, ok := t.Underlying().(*types.Struct) 109 | 110 | if !ok { 111 | return 112 | } 113 | 114 | // Don't report an error if: 115 | // 1. This composite literal contains no fields and 116 | // 2. It's in a return statement and 117 | // 3. The return statement contains a non-nil error 118 | if len(compositeLit.Elts) == 0 { 119 | // Check if this composite is one of the results the last return statement 120 | isInResults := false 121 | if returnStmt != nil { 122 | for _, result := range returnStmt.Results { 123 | compareComposite, ok := result.(*ast.CompositeLit) 124 | if ok { 125 | if compareComposite == compositeLit { 126 | isInResults = true 127 | } 128 | } 129 | } 130 | } 131 | nonNilError := false 132 | if isInResults { 133 | // Check if any of the results has an error type and if that error is set to non-nil (if it's set to nil, the type would be "untyped nil") 134 | for _, result := range returnStmt.Results { 135 | if pass.TypesInfo.TypeOf(result).String() == "error" { 136 | nonNilError = true 137 | } 138 | } 139 | } 140 | 141 | if nonNilError { 142 | return 143 | } 144 | } 145 | 146 | samePackage := strings.HasPrefix(t.String(), pass.Pkg.Path()+".") 147 | 148 | missing := []string{} 149 | 150 | for i := 0; i < str.NumFields(); i++ { 151 | fieldName := str.Field(i).Name() 152 | exists := false 153 | 154 | if !samePackage && !str.Field(i).Exported() { 155 | continue 156 | } 157 | 158 | for eIndex, e := range compositeLit.Elts { 159 | if k, ok := e.(*ast.KeyValueExpr); ok { 160 | if i, ok := k.Key.(*ast.Ident); ok { 161 | if i.Name == fieldName { 162 | exists = true 163 | break 164 | } 165 | } 166 | } else { 167 | if eIndex == i { 168 | exists = true 169 | break 170 | } 171 | } 172 | } 173 | 174 | if !exists { 175 | missing = append(missing, fieldName) 176 | } 177 | } 178 | 179 | if len(missing) == 1 { 180 | pass.Reportf(node.Pos(), "%s is missing in %s", missing[0], name) 181 | } else if len(missing) > 1 { 182 | pass.Reportf(node.Pos(), "%s are missing in %s", strings.Join(missing, ", "), name) 183 | } 184 | }) 185 | 186 | return nil, nil 187 | } 188 | --------------------------------------------------------------------------------