internal/fetch: refactor package loading

loadPackageWithBuildContext now returns only build-context-specific
values, not an entire goPackage. Instead, the goPackage is assembled
inside the loop over build contexts in loadPackage. This clarifies
the package-building logic and allows us to delete mergePackages.

For golang/go#37232

Change-Id: Ia46ca309f0ad30e30ba6f8dd8f8e797139d7399a
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/289673
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/fetch/load.go b/internal/fetch/load.go
index eea5463..ed48a11 100644
--- a/internal/fetch/load.go
+++ b/internal/fetch/load.go
@@ -56,7 +56,7 @@
 	defer derrors.Wrap(&err, "loadPackage(ctx, zipGoFiles, %q, sourceInfo, modInfo)", innerPath)
 	ctx, span := trace.StartSpan(ctx, "fetch.loadPackage")
 	defer span.End()
-	var pkgs []*goPackage
+
 	// Make a map with all the zip file contents.
 	files := make(map[string][]byte)
 	for _, f := range zipGoFiles {
@@ -68,91 +68,100 @@
 		files[name] = b
 	}
 
+	modulePath := modInfo.ModulePath
+	importPath := path.Join(modulePath, innerPath)
+	if modulePath == stdlib.ModulePath {
+		importPath = innerPath
+	}
+	v1path := internal.V1Path(importPath, modulePath)
+
+	var pkg *goPackage
 	for _, bc := range internal.BuildContexts {
-		pkg, err := loadPackageWithBuildContext(ctx, bc.GOOS, bc.GOARCH, files, innerPath, sourceInfo, modInfo)
+		name, imports, synopsis, source, err := loadPackageWithBuildContext(ctx, bc.GOOS, bc.GOARCH, files, innerPath, sourceInfo, modInfo)
 		switch {
 		case errors.Is(err, derrors.NotFound):
 			// No package for this build context.
 			continue
 		case errors.Is(err, godoc.ErrTooLarge):
-			// pkg should be non-nil. Remember the error and return the package
-			// for this build context; ignore the others.
-			if pkg == nil {
-				return nil, errors.New("unexpected nil package")
-			}
-			pkg.err = err
-			return pkg, nil
+			// The doc for this build context is too large. Remember that and
+			// return the package for this build context; ignore the others.
+			return &goPackage{
+				err:     err,
+				path:    importPath,
+				v1path:  v1path,
+				name:    name,
+				imports: imports,
+				docs: []*internal.Documentation{{
+					GOOS:     bc.GOOS,
+					GOARCH:   bc.GOARCH,
+					Synopsis: synopsis,
+					Source:   source,
+				}},
+			}, nil
 		case err != nil:
 			// Serious error. Fail.
 			return nil, err
 		default:
-			// No error. Remember the package.
-			pkgs = append(pkgs, pkg)
-		}
-	}
-	return mergePackages(pkgs)
-}
-
-// mergePackages combines multiple packages from different build contexts into
-// one.
-func mergePackages(pkgs []*goPackage) (*goPackage, error) {
-	if len(pkgs) == 0 {
-		return nil, nil
-	}
-	// If any build context has an error, return that one.
-	for _, pkg := range pkgs {
-		if pkg.err != nil {
-			return pkg, nil
-		}
-	}
-
-	// Use everything from the first package, just merge the docs.
-	result := pkgs[0]
-	for _, pkg := range pkgs[1:] {
-		if pkg.name != result.name {
-			// All the build environments should use the same package name. Although
+			// No error.
+			if pkg == nil {
+				pkg = &goPackage{
+					path:    importPath,
+					v1path:  v1path,
+					name:    name,
+					imports: imports, // Use the imports from the first successful build context.
+				}
+			}
+			// All the build contexts should use the same package name. Although
 			// it's technically legal for different build tags to result in different
 			// package names, it's not something we support.
-			return nil, &BadPackageError{
-				Err: fmt.Errorf("more than one package name (%q and %q)", result.name, pkg.name),
+			if name != pkg.name {
+				return nil, &BadPackageError{
+					Err: fmt.Errorf("more than one package name (%q and %q)", pkg.name, name),
+				}
 			}
+			pkg.docs = append(pkg.docs, &internal.Documentation{
+				GOOS:     bc.GOOS,
+				GOARCH:   bc.GOARCH,
+				Synopsis: synopsis,
+				Source:   source,
+			})
 		}
-		result.docs = append(result.docs, pkg.docs[0])
 	}
-	return result, nil
+	return pkg, nil
 }
 
 // httpPost allows package fetch tests to stub out playground URL fetches.
 var httpPost = http.Post
 
-// loadPackageWithBuildContext loads a Go package made of .go files in zipGoFiles
-// using a build context constructed from the given GOOS and GOARCH values.
-// modulePath is stdlib.ModulePath for the Go standard library and the module
-// path for all other modules. innerPath is the path of the Go package directory
-// relative to the module root.
+// loadPackageWithBuildContext loads a Go package made of .go files in
+// files using a build context constructed from the given GOOS and GOARCH
+// values. modulePath is stdlib.ModulePath for the Go standard library and the
+// module path for all other modules. innerPath is the path of the Go package
+// directory relative to the module root. The files argument must contain only
+// .go files that have been verified to be of reasonable size.
 //
-// zipGoFiles must contain only .go files that have been verified
-// to be of reasonable size.
+// It returns the list of imports, the package synopsis, and the serialized
+// source (AST) for the package.
 //
-// The returned goPackage.licenses field is not populated.
+// It returns an error with NotFound in its chain if the directory doesn't
+// contain a Go package or all .go files have been excluded by constraints. A
+// *BadPackageError error is returned if the directory contains .go files but do
+// not make up a valid package.
 //
-// It returns a nil goPackage if the directory doesn't contain a Go package
-// or all .go files have been excluded by constraints.
-// A *BadPackageError error is returned if the directory
-// contains .go files but do not make up a valid package.
-func loadPackageWithBuildContext(ctx context.Context, goos, goarch string, files map[string][]byte, innerPath string, sourceInfo *source.Info, modInfo *godoc.ModuleInfo) (_ *goPackage, err error) {
+// If it returns an error with ErrTooLarge in its chain, the returned name is
+// still valid.
+func loadPackageWithBuildContext(ctx context.Context, goos, goarch string, files map[string][]byte, innerPath string, sourceInfo *source.Info, modInfo *godoc.ModuleInfo) (name string, imports []string, synopsis string, source []byte, err error) {
 	modulePath := modInfo.ModulePath
 	defer derrors.Wrap(&err, "loadPackageWithBuildContext(%q, %q, files, %q, %q, %+v)",
 		goos, goarch, innerPath, modulePath, sourceInfo)
 
 	packageName, goFiles, fset, err := loadFilesWithBuildContext(innerPath, goos, goarch, files)
 	if err != nil {
-		return nil, err
+		return "", nil, "", nil, err
 	}
 	docPkg := godoc.NewPackage(fset, goos, goarch, modInfo.ModulePackages)
 	for _, pf := range goFiles {
-		var removeNodes bool
-		removeNodes = true
+		removeNodes := true
 		// Don't strip the seemingly unexported functions from the builtin package;
 		// they are actually Go builtins like make, new, etc.
 		if modulePath == stdlib.ModulePath && innerPath == "builtin" {
@@ -164,36 +173,21 @@
 	// Encode first, because Render messes with the AST.
 	src, err := docPkg.Encode(ctx)
 	if err != nil {
-		return nil, err
+		return "", nil, "", nil, err
 	}
 
-	synopsis, imports, _, err := docPkg.Render(ctx, innerPath, sourceInfo, modInfo, goos, goarch)
+	synopsis, imports, _, err = docPkg.Render(ctx, innerPath, sourceInfo, modInfo, goos, goarch)
 	if err != nil && !errors.Is(err, godoc.ErrTooLarge) {
-		return nil, err
+		return "", nil, "", nil, err
 	}
-	importPath := path.Join(modulePath, innerPath)
-	if modulePath == stdlib.ModulePath {
-		importPath = innerPath
-	}
-	v1path := internal.V1Path(importPath, modulePath)
-	return &goPackage{
-		path:    importPath,
-		name:    packageName,
-		v1path:  v1path,
-		imports: imports,
-		docs: []*internal.Documentation{{
-			GOOS:     goos,
-			GOARCH:   goarch,
-			Synopsis: synopsis,
-			Source:   src,
-		}},
-	}, err
+	return packageName, imports, synopsis, src, err
 }
 
 // loadFilesWithBuildContext loads all the Go files at innerPath that match goos
 // and goarch in the zip. It returns the package name as it occurs in the
 // source, a map of the ASTs of all the Go files, and the token.FileSet used for
 // parsing.
+// If there are no non-test Go files, it returns a NotFound error.
 func loadFilesWithBuildContext(innerPath, goos, goarch string, allFiles map[string][]byte) (pkgName string, fileMap map[string]*ast.File, _ *token.FileSet, _ error) {
 	// Apply build constraints to get a map from matching file names to their contents.
 	files, err := matchingFiles(goos, goarch, allFiles)
diff --git a/internal/fetch/load_test.go b/internal/fetch/load_test.go
index 21bfbd3..7503ef2 100644
--- a/internal/fetch/load_test.go
+++ b/internal/fetch/load_test.go
@@ -8,9 +8,6 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-cmp/cmp/cmpopts"
-	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/godoc"
 	"golang.org/x/pkgsite/internal/testing/testhelper"
 )
 
@@ -92,85 +89,3 @@
 		})
 	}
 }
-
-func TestMergePackages(t *testing.T) {
-	doc1 := &internal.Documentation{
-		GOOS:     "linux",
-		GOARCH:   "amd64",
-		Synopsis: "s1",
-	}
-	doc2 := &internal.Documentation{
-		GOOS:     "js",
-		GOARCH:   "wasm",
-		Synopsis: "s2",
-	}
-
-	pkg := func(name, imp string, doc *internal.Documentation, err error) *goPackage {
-		return &goPackage{
-			name:    name,
-			imports: []string{imp},
-			docs:    []*internal.Documentation{doc},
-			err:     err,
-		}
-	}
-
-	for _, test := range []struct {
-		name    string
-		pkgs    []*goPackage
-		want    *goPackage
-		wantErr bool
-	}{
-		{
-			name:    "no packages",
-			pkgs:    nil,
-			want:    nil,
-			wantErr: false,
-		},
-		{
-			name: "one package",
-			pkgs: []*goPackage{pkg("name1", "imp1", doc1, nil)},
-			want: pkg("name1", "imp1", doc1, nil),
-		},
-		{
-			name: "two packages",
-			pkgs: []*goPackage{
-				pkg("name1", "imp1", doc1, nil),
-				pkg("name1", "imp2", doc2, nil),
-			},
-			want: &goPackage{
-				name:    "name1",
-				imports: []string{"imp1"},                      // keep the first one
-				docs:    []*internal.Documentation{doc1, doc2}, // keep both, in order
-			},
-		},
-		{
-			name: "one package has err",
-			pkgs: []*goPackage{
-				pkg("name1", "imp1", doc1, nil),
-				pkg("name1", "imp2", doc2, godoc.ErrTooLarge),
-			},
-			want: pkg("name1", "imp2", doc2, godoc.ErrTooLarge), // return one with err
-		},
-		{
-			name: "different names",
-			pkgs: []*goPackage{
-				pkg("name1", "imp1", doc1, nil),
-				pkg("name2", "imp2", doc2, nil),
-			},
-			wantErr: true,
-		},
-	} {
-		t.Run(test.name, func(t *testing.T) {
-			got, err := mergePackages(test.pkgs)
-			if err != nil {
-				if !test.wantErr {
-					t.Fatalf("got %v, want no error", err)
-				}
-				return
-			}
-			if diff := cmp.Diff(test.want, got, cmp.AllowUnexported(goPackage{}), cmpopts.EquateErrors()); diff != "" {
-				t.Errorf("mismatch (-want, +got):\n%s", diff)
-			}
-		})
-	}
-}