1
2
3
4
5
6
7 package httpresponse
8
9 import (
10 "go/ast"
11 "go/types"
12
13 "golang.org/x/tools/go/analysis"
14 "golang.org/x/tools/go/analysis/passes/inspect"
15 "golang.org/x/tools/go/ast/inspector"
16 "golang.org/x/tools/internal/typesinternal"
17 )
18
19 const Doc = `check for mistakes using HTTP responses
20
21 A common mistake when using the net/http package is to defer a function
22 call to close the http.Response Body before checking the error that
23 determines whether the response is valid:
24
25 resp, err := http.Head(url)
26 defer resp.Body.Close()
27 if err != nil {
28 log.Fatal(err)
29 }
30 // (defer statement belongs here)
31
32 This checker helps uncover latent nil dereference bugs by reporting a
33 diagnostic for such mistakes.`
34
35 var Analyzer = &analysis.Analyzer{
36 Name: "httpresponse",
37 Doc: Doc,
38 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpresponse",
39 Requires: []*analysis.Analyzer{inspect.Analyzer},
40 Run: run,
41 }
42
43 func run(pass *analysis.Pass) (any, error) {
44 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
45
46
47
48 if !typesinternal.Imports(pass.Pkg, "net/http") {
49 return nil, nil
50 }
51
52 nodeFilter := []ast.Node{
53 (*ast.CallExpr)(nil),
54 }
55 inspect.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) bool {
56 if !push {
57 return true
58 }
59 call := n.(*ast.CallExpr)
60 if !isHTTPFuncOrMethodOnClient(pass.TypesInfo, call) {
61 return true
62 }
63
64
65
66 stmts, ncalls := restOfBlock(stack)
67 if len(stmts) < 2 {
68
69 return true
70 }
71
72
73
74 if ncalls > 1 {
75 return true
76 }
77
78 asg, ok := stmts[0].(*ast.AssignStmt)
79 if !ok {
80 return true
81 }
82
83 resp := rootIdent(asg.Lhs[0])
84 if resp == nil {
85 return true
86 }
87
88 def, ok := stmts[1].(*ast.DeferStmt)
89 if !ok {
90 return true
91 }
92 root := rootIdent(def.Call.Fun)
93 if root == nil {
94 return true
95 }
96
97 if resp.Obj == root.Obj {
98 pass.ReportRangef(root, "using %s before checking for errors", resp.Name)
99 }
100 return true
101 })
102 return nil, nil
103 }
104
105
106
107
108 func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool {
109 fun, _ := expr.Fun.(*ast.SelectorExpr)
110 sig, _ := info.Types[fun].Type.(*types.Signature)
111 if sig == nil {
112 return false
113 }
114
115 res := sig.Results()
116 if res.Len() != 2 {
117 return false
118 }
119 isPtr, named := typesinternal.ReceiverNamed(res.At(0))
120 if !isPtr || named == nil || !typesinternal.IsTypeNamed(named, "net/http", "Response") {
121 return false
122 }
123
124 errorType := types.Universe.Lookup("error").Type()
125 if !types.Identical(res.At(1).Type(), errorType) {
126 return false
127 }
128
129 typ := info.Types[fun.X].Type
130 if typ == nil {
131 id, ok := fun.X.(*ast.Ident)
132 return ok && id.Name == "http"
133 }
134
135 if typesinternal.IsTypeNamed(typ, "net/http", "Client") {
136 return true
137 }
138 ptr, ok := types.Unalias(typ).(*types.Pointer)
139 return ok && typesinternal.IsTypeNamed(ptr.Elem(), "net/http", "Client")
140 }
141
142
143
144
145 func restOfBlock(stack []ast.Node) ([]ast.Stmt, int) {
146 var ncalls int
147 for i := len(stack) - 1; i >= 0; i-- {
148 if b, ok := stack[i].(*ast.BlockStmt); ok {
149 for j, v := range b.List {
150 if v == stack[i+1] {
151 return b.List[j:], ncalls
152 }
153 }
154 break
155 }
156
157 if _, ok := stack[i].(*ast.CallExpr); ok {
158 ncalls++
159 }
160 }
161 return nil, 0
162 }
163
164
165 func rootIdent(n ast.Node) *ast.Ident {
166 switch n := n.(type) {
167 case *ast.SelectorExpr:
168 return rootIdent(n.X)
169 case *ast.Ident:
170 return n
171 default:
172 return nil
173 }
174 }
175
View as plain text