go/analysis/passes/httpresponse: inspect enclosing context of resp, err

The resp, err results of, say, http.Get can immediatelly be passed to a
helper function that checks err and returns resp. In that case, it is ok
for defer to occur immediately after.

The checker currently assumes that a call to, say, http.Get is the only
call made when a resp is returned. The fix is to check if the original
call is made as a part of another (helper) call and, if so, punt.

For golang/go#52661

Change-Id: If9dc4815013476de381fe69548d1fb9c04aa9fd9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/404656
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go
index fd9e2af..092ac75 100644
--- a/go/analysis/passes/httpresponse/httpresponse.go
+++ b/go/analysis/passes/httpresponse/httpresponse.go
@@ -62,7 +62,13 @@
 
 		// Find the innermost containing block, and get the list
 		// of statements starting with the one containing call.
-		stmts := restOfBlock(stack)
+		stmts, withinAnotherCall := restOfBlock(stack)
+		if withinAnotherCall {
+			// We skip cases when the results of a call to http member
+			// are passed directly to another call, as that later call
+			// could check err != nil and create false positives (#52661).
+			return true
+		}
 		if len(stmts) < 2 {
 			return true // the call to the http function is the last statement of the block.
 		}
@@ -71,6 +77,7 @@
 		if !ok {
 			return true // the first statement is not assignment.
 		}
+
 		resp := rootIdent(asg.Lhs[0])
 		if resp == nil {
 			return true // could not find the http.Response in the assignment.
@@ -129,21 +136,34 @@
 	return ok && isNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client.
 }
 
-// restOfBlock, given a traversal stack, finds the innermost containing
-// block and returns the suffix of its statements starting with the
-// current node (the last element of stack).
-func restOfBlock(stack []ast.Node) []ast.Stmt {
+// restOfBlock, given a traversal stack, checks if the current node
+// (the last element of stack) appears as an argument to another call.
+// If not, it finds the innermost containing block and returns the
+// suffix of its statements starting with the current node. Otherwise,
+// returns an empty slice.
+func restOfBlock(stack []ast.Node) ([]ast.Stmt, bool) {
 	for i := len(stack) - 1; i >= 0; i-- {
+		// If the current node appears within another call, then
+		// this has to happen within the same block. We can thus
+		// immediately return on whichever we see first, a block
+		// statement or a call statement.
+
 		if b, ok := stack[i].(*ast.BlockStmt); ok {
 			for j, v := range b.List {
 				if v == stack[i+1] {
-					return b.List[j:]
+					return b.List[j:], false
 				}
 			}
 			break
 		}
+
+		// The call to an http member currently analyzed is at len(stack)-1.
+		if _, ok := stack[i].(*ast.CallExpr); ok && i != len(stack)-1 {
+			return nil, true // e.g. "resp, err := wrap(http.Get(...))"
+		}
+
 	}
-	return nil
+	return nil, false
 }
 
 // rootIdent finds the root identifier x in a chain of selections x.y.z, or nil if not found.
diff --git a/go/analysis/passes/httpresponse/httpresponse_test.go b/go/analysis/passes/httpresponse/httpresponse_test.go
index 14e1667..34dc78c 100644
--- a/go/analysis/passes/httpresponse/httpresponse_test.go
+++ b/go/analysis/passes/httpresponse/httpresponse_test.go
@@ -5,10 +5,11 @@
 package httpresponse_test
 
 import (
+	"testing"
+
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/httpresponse"
 	"golang.org/x/tools/internal/typeparams"
-	"testing"
 )
 
 func Test(t *testing.T) {
diff --git a/go/analysis/passes/httpresponse/testdata/src/a/a.go b/go/analysis/passes/httpresponse/testdata/src/a/a.go
index df7703f..de41212 100644
--- a/go/analysis/passes/httpresponse/testdata/src/a/a.go
+++ b/go/analysis/passes/httpresponse/testdata/src/a/a.go
@@ -83,3 +83,30 @@
 		log.Fatal(err)
 	}
 }
+
+func goodUnwrapResp() {
+	unwrapResp := func(resp *http.Response, err error) *http.Response {
+		if err != nil {
+			panic(err)
+		}
+		return resp
+	}
+	resp := unwrapResp(http.Get("https://golang.org"))
+	// It is ok to call defer here immediately as err has
+	// been checked in unwrapResp (see #52661).
+	defer resp.Body.Close()
+}
+
+func badUnwrapResp() {
+	unwrapResp := func(resp *http.Response, err error) string {
+		if err != nil {
+			panic(err)
+		}
+		return "https://golang.org/" + resp.Status
+	}
+	resp, err := http.Get(unwrapResp(http.Get("https://golang.org")))
+	defer resp.Body.Close() // want "using resp before checking for errors"
+	if err != nil {
+		log.Fatal(err)
+	}
+}