From 72e137e34404348c47482744ebca3f17fd5ae9aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gianguido=20Sor=C3=A0?= <me@gsora.xyz>
Date: Sat, 11 May 2019 00:21:15 +0200
Subject: [PATCH] Added "bodyclose" support

This commit adds full support for bodyclose linter
(https://github.com/timakin/bodyclose), which checks if an `http.Body`
element is correctly closed after usage.

Since it can be used via `go/analysis', I followed the `govet' example
as suggested by https://github.com/golangci/golangci-lint/wiki/How-to-add-a-custom-linter.

This commit is fully tested, and contains a (flawed) test program which
calls `http.Get()' on `https://google.com' and does not closes its
corresponding `http.Body'.
---
 README.md                                     |   3 +
 go.mod                                        |   1 +
 go.sum                                        |   6 +
 pkg/golinters/bodyclose.go                    |  20 +
 pkg/lint/lintersdb/manager.go                 |   5 +
 test/testdata/bodyclose.go                    |  12 +
 .../gostaticanalysis/analysisutil/LICENSE     |  21 ++
 .../gostaticanalysis/analysisutil/README.md   |  10 +
 .../gostaticanalysis/analysisutil/file.go     |  18 +
 .../gostaticanalysis/analysisutil/go.mod      |   5 +
 .../gostaticanalysis/analysisutil/go.sum      |   6 +
 .../gostaticanalysis/analysisutil/pkg.go      |  26 ++
 .../gostaticanalysis/analysisutil/ssa.go      |  31 ++
 vendor/github.com/timakin/bodyclose/LICENSE   |  21 ++
 .../bodyclose/passes/bodyclose/bodyclose.go   | 352 ++++++++++++++++++
 vendor/modules.txt                            |   4 +
 16 files changed, 541 insertions(+)
 create mode 100644 pkg/golinters/bodyclose.go
 create mode 100644 test/testdata/bodyclose.go
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/LICENSE
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/README.md
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/file.go
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/go.mod
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/go.sum
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/pkg.go
 create mode 100644 vendor/github.com/gostaticanalysis/analysisutil/ssa.go
 create mode 100644 vendor/github.com/timakin/bodyclose/LICENSE
 create mode 100644 vendor/github.com/timakin/bodyclose/passes/bodyclose/bodyclose.go

diff --git a/README.md b/README.md
index 09d4e015..c09ff0c6 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: 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]
@@ -416,6 +417,7 @@ golangci-lint help linters
 
 ### Disabled By Default Linters (`-E/--enable`)
 
+- [bodyclose](https://github.com/timakin/bodyclose) - 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
@@ -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/go.mod b/go.mod
index 919e7b7c..db0ed1cc 100644
--- a/go.mod
+++ b/go.mod
@@ -49,6 +49,7 @@ require (
 	github.com/spf13/pflag v1.0.1
 	github.com/spf13/viper v1.0.2
 	github.com/stretchr/testify v1.2.2
+	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 935d55d7..a6184e29 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=
@@ -150,6 +152,8 @@ github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso=
 github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+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=
@@ -177,6 +181,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/pkg/golinters/bodyclose.go b/pkg/golinters/bodyclose.go
new file mode 100644
index 00000000..f7ea02c4
--- /dev/null
+++ b/pkg/golinters/bodyclose.go
@@ -0,0 +1,20 @@
+package golinters
+
+import (
+	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
+	"github.com/timakin/bodyclose/passes/bodyclose"
+	"golang.org/x/tools/go/analysis"
+)
+
+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..8df98def 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(). // TODO: extract from the linter config and don't build SSA, just use LoadAllSyntax mode
+			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.
+
+<!-- links -->
+[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 bc15d808..cc1dff30 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
@@ -179,6 +181,8 @@ github.com/spf13/viper
 # github.com/stretchr/testify v1.2.2
 github.com/stretchr/testify/assert
 github.com/stretchr/testify/require
+# 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/net v0.0.0-20190313220215-9f648a60d977