diff --git a/.golangci.yml b/.golangci.yml index 77f11362..3134f166 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -61,6 +61,6 @@ issues: # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration service: - golangci-lint-version: 1.15.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.16.x # use the fixed version to not introduce new linters unexpectedly prepare: - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/README.md b/README.md index cc3dc1d3..da5e5129 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ and the following linters are disabled by default: $ golangci-lint help linters ... Disabled by default linters: +bodyclose: checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] depguard: Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] dupl: Tool for code clone detection [fast: true, auto-fix: false] gochecknoglobals: Checks that no globals are present in Go code [fast: true, auto-fix: false] @@ -344,7 +345,7 @@ Read [this section](#internals) for details. ### Memory Usage of Golangci-lint -A trade-off between memory usage and execution time can be controlled by [`GOCC`](https://golang.org/pkg/runtime/#hdr-Environment_Variables) environment variable. +A trade-off between memory usage and execution time can be controlled by [`GOGC`](https://golang.org/pkg/runtime/#hdr-Environment_Variables) environment variable. Less `GOGC` values trigger garbage collection more frequently and golangci-lint consumes less memory and more CPU. Below is the trade-off table for running on this repo: |`GOGC`|Peak Memory, GB|Executon Time, s| @@ -416,6 +417,7 @@ golangci-lint help linters ### Disabled By Default Linters (`-E/--enable`) +- [bodyclose](https://github.com/timakin/bodyclose) - checks whether HTTP response body is closed successfully - [golint](https://github.com/golang/lint) - Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes - [stylecheck](https://github.com/dominikh/go-tools/tree/master/stylecheck) - Stylecheck is a replacement for golint - [gosec](https://github.com/securego/gosec) - Inspects source code for security problems @@ -871,7 +873,7 @@ issues: # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration service: - golangci-lint-version: 1.15.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.16.x # use the fixed version to not introduce new linters unexpectedly prepare: - echo "here I can run custom commands, but no preparation needed for this repo" ``` @@ -926,8 +928,8 @@ Short answer: go 1.11 and newer are oficially supported. Long answer: 1. go < 1.9 isn't supported 2. go 1.9 is supported by golangci-lint <= v1.10.2 -3. go 1.10 is oficially supported by golangci-lint <= 1.15.0. -4. go1.11 and go1.12 are oficially supported by the latest version of golangci-lint. +3. go 1.10 is officially supported by golangci-lint <= 1.15.0. +4. go1.11 and go1.12 are officially supported by the latest version of golangci-lint. **`golangci-lint` doesn't work** @@ -946,6 +948,7 @@ Thanks to [alecthomas/gometalinter](https://github.com/alecthomas/gometalinter) Thanks to [bradleyfalzon/revgrep](https://github.com/bradleyfalzon/revgrep) for cool diff tool. Thanks to developers and authors of used linters: +- [timakin](https://github.com/timakin) - [kisielk](https://github.com/kisielk) - [golang](https://github.com/golang) - [dominikh](https://github.com/dominikh) diff --git a/README.tmpl.md b/README.tmpl.md index 26121406..bae6b95d 100644 --- a/README.tmpl.md +++ b/README.tmpl.md @@ -313,7 +313,7 @@ Read [this section](#internals) for details. ### Memory Usage of Golangci-lint -A trade-off between memory usage and execution time can be controlled by [`GOCC`](https://golang.org/pkg/runtime/#hdr-Environment_Variables) environment variable. +A trade-off between memory usage and execution time can be controlled by [`GOGC`](https://golang.org/pkg/runtime/#hdr-Environment_Variables) environment variable. Less `GOGC` values trigger garbage collection more frequently and golangci-lint consumes less memory and more CPU. Below is the trade-off table for running on this repo: |`GOGC`|Peak Memory, GB|Executon Time, s| @@ -475,8 +475,8 @@ Short answer: go 1.11 and newer are oficially supported. Long answer: 1. go < 1.9 isn't supported 2. go 1.9 is supported by golangci-lint <= v1.10.2 -3. go 1.10 is oficially supported by golangci-lint <= 1.15.0. -4. go1.11 and go1.12 are oficially supported by the latest version of golangci-lint. +3. go 1.10 is officially supported by golangci-lint <= 1.15.0. +4. go1.11 and go1.12 are officially supported by the latest version of golangci-lint. **`golangci-lint` doesn't work** diff --git a/go.mod b/go.mod index 69a4bd6b..c2486e95 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/spf13/viper v1.0.2 github.com/stretchr/testify v1.2.2 github.com/valyala/quicktemplate v1.1.1 + github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect golang.org/x/net v0.0.0-20190313220215-9f648a60d977 // indirect golang.org/x/sys v0.0.0-20190312061237-fead79001313 // indirect diff --git a/go.sum b/go.sum index 608af03f..d08aa2d4 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -160,6 +162,8 @@ github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk github.com/valyala/quicktemplate v1.1.1 h1:C58y/wN0FMTi2PR0n3onltemfFabany53j7M6SDDB8k= github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM= +github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -188,6 +192,8 @@ golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420000508-685fecacd0a0 h1:pa1CyBALPFjblgkNQp7T7gEcFcG/GOG5Ck8IcnSVWGs= golang.org/x/tools v0.0.0-20190420000508-685fecacd0a0/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= diff --git a/install.sh b/install.sh index 8fd253af..dc993a8a 100644 --- a/install.sh +++ b/install.sh @@ -172,7 +172,8 @@ log_crit() { uname_os() { os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in - msys_nt) os="windows" ;; + msys_nt*) os="windows" ;; + mingw*) os="windows" ;; esac echo "$os" } diff --git a/pkg/golinters/bodyclose.go b/pkg/golinters/bodyclose.go new file mode 100644 index 00000000..62655081 --- /dev/null +++ b/pkg/golinters/bodyclose.go @@ -0,0 +1,21 @@ +package golinters + +import ( + "github.com/timakin/bodyclose/passes/bodyclose" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" +) + +func NewBodyclose() *goanalysis.Linter { + analyzers := []*analysis.Analyzer{ + bodyclose.Analyzer, + } + + return goanalysis.NewLinter( + "bodyclose", + "checks whether HTTP response body is closed successfully", + analyzers, + nil, + ) +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index 79185ae3..dd5c57ca 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -89,6 +89,11 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { WithSpeed(4). WithAlternativeNames("vet", "vetshadow"). WithURL("https://golang.org/cmd/vet/"), + linter.NewConfig(golinters.NewBodyclose()). + WithSSA(). + WithPresets(linter.PresetPerformance, linter.PresetBugs). + WithSpeed(4). + WithURL("https://github.com/timakin/bodyclose"), linter.NewConfig(golinters.Errcheck{}). WithTypeInfo(). WithPresets(linter.PresetBugs). diff --git a/test/testdata/bodyclose.go b/test/testdata/bodyclose.go new file mode 100644 index 00000000..7e4d874a --- /dev/null +++ b/test/testdata/bodyclose.go @@ -0,0 +1,12 @@ +//args: -Ebodyclose +package testdata + +import ( + "io/ioutil" + "net/http" +) + +func BodycloseNotClosed() { + resp, _ := http.Get("https://google.com") // ERROR "bodyclose: response body must be closed" + _, _ = ioutil.ReadAll(resp.Body) +} diff --git a/vendor/github.com/gostaticanalysis/analysisutil/LICENSE b/vendor/github.com/gostaticanalysis/analysisutil/LICENSE new file mode 100644 index 00000000..bf7e33db --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 GoStaticAnalysis + +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/vendor/github.com/gostaticanalysis/analysisutil/README.md b/vendor/github.com/gostaticanalysis/analysisutil/README.md new file mode 100644 index 00000000..c031e16e --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/README.md @@ -0,0 +1,10 @@ +# analysisutil + +[![godoc.org][godoc-badge]][godoc] + +Utilities for x/tools/go/analysis package. + + +[godoc]: https://godoc.org/github.com/gostaticanalysis/analysisutil +[godoc-badge]: https://img.shields.io/badge/godoc-reference-4F73B3.svg?style=flat-square&label=%20godoc.org + diff --git a/vendor/github.com/gostaticanalysis/analysisutil/file.go b/vendor/github.com/gostaticanalysis/analysisutil/file.go new file mode 100644 index 00000000..2aeca1d9 --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/file.go @@ -0,0 +1,18 @@ +package analysisutil + +import ( + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" +) + +// File finds *ast.File in pass.Files by pos. +func File(pass *analysis.Pass, pos token.Pos) *ast.File { + for _, f := range pass.Files { + if f.Pos() <= pos && pos <= f.End() { + return f + } + } + return nil +} diff --git a/vendor/github.com/gostaticanalysis/analysisutil/go.mod b/vendor/github.com/gostaticanalysis/analysisutil/go.mod new file mode 100644 index 00000000..7e3e55be --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/go.mod @@ -0,0 +1,5 @@ +module github.com/gostaticanalysis/analysisutil + +go 1.12 + +require golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5 diff --git a/vendor/github.com/gostaticanalysis/analysisutil/go.sum b/vendor/github.com/gostaticanalysis/analysisutil/go.sum new file mode 100644 index 00000000..16c8787c --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/go.sum @@ -0,0 +1,6 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5 h1:ZcPpqKMdoZeNQ/4GHlyY4COf8n8SmpPv6mcqF1+VPSM= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/vendor/github.com/gostaticanalysis/analysisutil/pkg.go b/vendor/github.com/gostaticanalysis/analysisutil/pkg.go new file mode 100644 index 00000000..c98710d1 --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/pkg.go @@ -0,0 +1,26 @@ +package analysisutil + +import ( + "go/types" + "strings" +) + +// RemoVendor removes vendoring infomation from import path. +func RemoveVendor(path string) string { + i := strings.Index(path, "vendor") + if i >= 0 { + return path[i+len("vendor")+1:] + } + return path +} + +// LookupFromImports finds an object from import paths. +func LookupFromImports(imports []*types.Package, path, name string) types.Object { + path = RemoveVendor(path) + for i := range imports { + if path == RemoveVendor(imports[i].Path()) { + return imports[i].Scope().Lookup(name) + } + } + return nil +} diff --git a/vendor/github.com/gostaticanalysis/analysisutil/ssa.go b/vendor/github.com/gostaticanalysis/analysisutil/ssa.go new file mode 100644 index 00000000..37915866 --- /dev/null +++ b/vendor/github.com/gostaticanalysis/analysisutil/ssa.go @@ -0,0 +1,31 @@ +package analysisutil + +import "golang.org/x/tools/go/ssa" + +// IfInstr returns *ssa.If which is contained in the block b. +// If the block b has not any if instruction, IfInstr returns nil. +func IfInstr(b *ssa.BasicBlock) *ssa.If { + if len(b.Instrs) == 0 { + return nil + } + + ifinstr, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If) + if !ok { + return nil + } + + return ifinstr +} + +// Phi returns phi values which are contained in the block b. +func Phi(b *ssa.BasicBlock) (phis []*ssa.Phi) { + for _, instr := range b.Instrs { + if phi, ok := instr.(*ssa.Phi); ok { + phis = append(phis, phi) + } else { + // no more phi + break + } + } + return +} diff --git a/vendor/github.com/timakin/bodyclose/LICENSE b/vendor/github.com/timakin/bodyclose/LICENSE new file mode 100644 index 00000000..6957f188 --- /dev/null +++ b/vendor/github.com/timakin/bodyclose/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Seiji Takahashi + +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/vendor/github.com/timakin/bodyclose/passes/bodyclose/bodyclose.go b/vendor/github.com/timakin/bodyclose/passes/bodyclose/bodyclose.go new file mode 100644 index 00000000..1ea79f14 --- /dev/null +++ b/vendor/github.com/timakin/bodyclose/passes/bodyclose/bodyclose.go @@ -0,0 +1,352 @@ +package bodyclose + +import ( + "fmt" + "go/ast" + "go/types" + "strconv" + "strings" + + "github.com/gostaticanalysis/analysisutil" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/buildssa" + "golang.org/x/tools/go/ssa" +) + +var Analyzer = &analysis.Analyzer{ + Name: "bodyclose", + Doc: Doc, + Run: new(runner).run, + Requires: []*analysis.Analyzer{ + buildssa.Analyzer, + }, +} + +const ( + Doc = "bodyclose checks whether HTTP response body is closed successfully" + + nethttpPath = "net/http" + closeMethod = "Close" +) + +type runner struct { + pass *analysis.Pass + resObj types.Object + resTyp *types.Pointer + bodyObj types.Object + closeMthd *types.Func + skipFile map[*ast.File]bool +} + +func (r *runner) run(pass *analysis.Pass) (interface{}, error) { + r.pass = pass + funcs := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA).SrcFuncs + + r.resObj = analysisutil.LookupFromImports(pass.Pkg.Imports(), nethttpPath, "Response") + if r.resObj == nil { + // skip checking + return nil, nil + } + + resNamed, ok := r.resObj.Type().(*types.Named) + if !ok { + return nil, fmt.Errorf("cannot find http.Response") + } + r.resTyp = types.NewPointer(resNamed) + + resStruct, ok := r.resObj.Type().Underlying().(*types.Struct) + if !ok { + return nil, fmt.Errorf("cannot find http.Response") + } + for i := 0; i < resStruct.NumFields(); i++ { + field := resStruct.Field(i) + if field.Id() == "Body" { + r.bodyObj = field + } + } + if r.bodyObj == nil { + return nil, fmt.Errorf("cannot find the object http.Response.Body") + } + bodyNamed := r.bodyObj.Type().(*types.Named) + bodyItrf := bodyNamed.Underlying().(*types.Interface) + for i := 0; i < bodyItrf.NumMethods(); i++ { + bmthd := bodyItrf.Method(i) + if bmthd.Id() == closeMethod { + r.closeMthd = bmthd + } + } + + r.skipFile = map[*ast.File]bool{} + for _, f := range funcs { + if r.noImportedNetHTTP(f) { + // skip this + continue + } + + // skip if the function is just referenced + var isreffunc bool + for i := 0; i < f.Signature.Results().Len(); i++ { + if f.Signature.Results().At(i).Type().String() == r.resTyp.String() { + isreffunc = true + } + } + if isreffunc { + continue + } + + for _, b := range f.Blocks { + for i := range b.Instrs { + pos := b.Instrs[i].Pos() + if r.isopen(b, i) { + pass.Reportf(pos, "response body must be closed") + } + } + } + } + + return nil, nil +} + +func (r *runner) isopen(b *ssa.BasicBlock, i int) bool { + call, ok := r.getReqCall(b.Instrs[i]) + if !ok { + return false + } + + if len(*call.Referrers()) == 0 { + return true + } + cRefs := *call.Referrers() + for _, cRef := range cRefs { + val, ok := r.getResVal(cRef) + if !ok { + continue + } + + if len(*val.Referrers()) == 0 { + return true + } + resRefs := *val.Referrers() + for _, resRef := range resRefs { + switch resRef := resRef.(type) { + case *ssa.Store: // Call in Closure function + if len(*resRef.Addr.Referrers()) == 0 { + return true + } + + for _, aref := range *resRef.Addr.Referrers() { + if c, ok := aref.(*ssa.MakeClosure); ok { + f := c.Fn.(*ssa.Function) + if r.noImportedNetHTTP(f) { + // skip this + return false + } + called := r.isClosureCalled(c) + + return r.calledInFunc(f, called) + } + + } + case *ssa.Call: // Indirect function call + if f, ok := resRef.Call.Value.(*ssa.Function); ok { + for _, b := range f.Blocks { + for i := range b.Instrs { + return r.isopen(b, i) + } + } + } + case *ssa.FieldAddr: // Normal reference to response entity + if resRef.Referrers() == nil { + return true + } + + bRefs := *resRef.Referrers() + + for _, bRef := range bRefs { + bOp, ok := r.getBodyOp(bRef) + if !ok { + continue + } + if len(*bOp.Referrers()) == 0 { + return true + } + ccalls := *bOp.Referrers() + for _, ccall := range ccalls { + if r.isCloseCall(ccall) { + return false + } + } + } + } + } + } + + return true +} + +func (r *runner) getReqCall(instr ssa.Instruction) (*ssa.Call, bool) { + call, ok := instr.(*ssa.Call) + if !ok { + return nil, false + } + if !strings.Contains(call.Type().String(), r.resTyp.String()) { + return nil, false + } + return call, true +} + +func (r *runner) getResVal(instr ssa.Instruction) (ssa.Value, bool) { + switch instr := instr.(type) { + case *ssa.FieldAddr: + if instr.X.Type().String() == r.resTyp.String() { + return instr.X.(ssa.Value), true + } + case ssa.Value: + if instr.Type().String() == r.resTyp.String() { + return instr, true + } + } + return nil, false +} + +func (r *runner) getBodyOp(instr ssa.Instruction) (*ssa.UnOp, bool) { + op, ok := instr.(*ssa.UnOp) + if !ok { + return nil, false + } + if op.Type() != r.bodyObj.Type() { + return nil, false + } + return op, true +} + +func (r *runner) isCloseCall(ccall ssa.Instruction) bool { + switch ccall := ccall.(type) { + case *ssa.Defer: + if ccall.Call.Method.Name() == r.closeMthd.Name() { + return true + } + case *ssa.Call: + if ccall.Call.Method.Name() == r.closeMthd.Name() { + return true + } + case *ssa.ChangeInterface: + if ccall.Type().String() == "io.Closer" { + closeMtd := ccall.Type().Underlying().(*types.Interface).Method(0) + crs := *ccall.Referrers() + for _, cs := range crs { + if cs, ok := cs.(*ssa.Defer); ok { + if val, ok := cs.Common().Value.(*ssa.Function); ok { + for _, b := range val.Blocks { + for _, instr := range b.Instrs { + if c, ok := instr.(*ssa.Call); ok { + if c.Call.Method == closeMtd { + return true + } + } + } + } + } + } + } + } + } + return false +} + +func (r *runner) isClosureCalled(c *ssa.MakeClosure) bool { + refs := *c.Referrers() + if len(refs) == 0 { + return false + } + for _, ref := range refs { + switch ref.(type) { + case *ssa.Call, *ssa.Defer: + return true + } + } + return false +} + +func (r *runner) noImportedNetHTTP(f *ssa.Function) (ret bool) { + obj := f.Object() + if obj == nil { + return false + } + + file := analysisutil.File(r.pass, obj.Pos()) + if file == nil { + return false + } + + if skip, has := r.skipFile[file]; has { + return skip + } + defer func() { + r.skipFile[file] = ret + }() + + for _, impt := range file.Imports { + path, err := strconv.Unquote(impt.Path.Value) + if err != nil { + continue + } + path = analysisutil.RemoveVendor(path) + if path == nethttpPath { + return false + } + } + + return true +} + +func (r *runner) calledInFunc(f *ssa.Function, called bool) bool { + for _, b := range f.Blocks { + for i, instr := range b.Instrs { + switch instr := instr.(type) { + case *ssa.UnOp: + refs := *instr.Referrers() + if len(refs) == 0 { + return true + } + for _, r := range refs { + if v, ok := r.(ssa.Value); ok { + if ptr, ok := v.Type().(*types.Pointer); !ok || !isNamedType(ptr.Elem(), "io", "ReadCloser") { + return true + } + vrefs := *v.Referrers() + for _, vref := range vrefs { + if vref, ok := vref.(*ssa.UnOp); ok { + vrefs := *vref.Referrers() + if len(vrefs) == 0 { + return true + } + for _, vref := range vrefs { + if c, ok := vref.(*ssa.Call); ok { + if c.Call.Method.Name() == closeMethod { + return !called + } + } + } + } + } + } + + } + default: + return r.isopen(b, i) || !called + } + } + } + return false +} + +// isNamedType reports whether t is the named type path.name. +func isNamedType(t types.Type, path, name string) bool { + n, ok := t.(*types.Named) + if !ok { + return false + } + obj := n.Obj() + return obj.Name() == name && obj.Pkg() != nil && obj.Pkg().Path() == path +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 583170f4..1ed4a3ab 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -106,6 +106,8 @@ github.com/golangci/prealloc github.com/golangci/revgrep # github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 github.com/golangci/unconvert +# github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 +github.com/gostaticanalysis/analysisutil # github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce github.com/hashicorp/hcl github.com/hashicorp/hcl/hcl/printer @@ -183,6 +185,8 @@ github.com/stretchr/testify/require github.com/valyala/bytebufferpool # github.com/valyala/quicktemplate v1.1.1 github.com/valyala/quicktemplate +# github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb +github.com/timakin/bodyclose/passes/bodyclose # golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a golang.org/x/crypto/ssh/terminal # golang.org/x/sys v0.0.0-20190312061237-fead79001313