go/packages: work around go list behavior for missing dependencies

If go/packages.Load tries to load a package with a missing dependency, it
should not return an error, but a package with an error set on it.  This is a
workaround for go list -e -compiled (or even just go list -e) returning a
non-zero exit status for packages with missing dependencies.

Change-Id: I2d7d848ae5133235f595baf7b30296077e891ee3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/170891
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/go/packages/golist.go b/go/packages/golist.go
index 3a0d4b0..5389849 100644
--- a/go/packages/golist.go
+++ b/go/packages/golist.go
@@ -777,6 +777,22 @@
 			return bytes.NewBufferString(output), nil
 		}
 
+		// Workaround for an instance of golang.org/issue/26755: go list -e  will return a non-zero exit
+		// status if there's a dependency on a package that doesn't exist. But it should return
+		// a zero exit status and set an error on that package.
+		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "no Go files in") {
+			// try to extract package name from string
+			stderrStr := stderr.String()
+			var importPath string
+			colon := strings.Index(stderrStr, ":")
+			if colon > 0 && strings.HasPrefix(stderrStr, "go build ") {
+				importPath = stderrStr[len("go build "):colon]
+			}
+			output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`,
+				importPath, strings.Trim(stderrStr, "\n"))
+			return bytes.NewBufferString(output), nil
+		}
+
 		// Export mode entails a build.
 		// If that build fails, errors appear on stderr
 		// (despite the -e flag) and the Export field is blank.
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go
index d9ba0f4..20a94a7 100644
--- a/go/packages/packages_test.go
+++ b/go/packages/packages_test.go
@@ -1828,6 +1828,28 @@
 	}
 }
 
+func TestMissingDependency(t *testing.T) { packagestest.TestAll(t, testMissingDependency) }
+func testMissingDependency(t *testing.T, exporter packagestest.Exporter) {
+	exported := packagestest.Export(t, exporter, []packagestest.Module{{
+		Name: "golang.org/fake",
+		Files: map[string]interface{}{
+			"a/a.go": `package a; import _ "this/package/doesnt/exist"`,
+		}}})
+	defer exported.Cleanup()
+
+	exported.Config.Mode = packages.LoadAllSyntax
+	pkgs, err := packages.Load(exported.Config, "golang.org/fake/a")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(pkgs) != 1 && pkgs[0].PkgPath != "golang.org/fake/a" {
+		t.Fatalf("packages.Load: want [golang.org/fake/a], got %v", pkgs)
+	}
+	if len(pkgs[0].Errors) == 0 {
+		t.Errorf("result of Load: want package with errors, got none: %+v", pkgs[0])
+	}
+}
+
 func errorMessages(errors []packages.Error) []string {
 	var msgs []string
 	for _, err := range errors {