bp2build: support Starlark rules and load statements.

This CL adds support to bp2build for declaring the location of the
Starlark rule definition when creating BazelTargetModules. This is
needed for non-native rules that needs to be loaded from .bzl files
somewhere in the tree.

Since load statements are aggregated at the top of the BUILD file, away
from the targets that actually use them, this CL also introduces an
abstraction to group BazelTargets together and compute their load
statements and target string representations separately, allowing load
statements to be decoupled and written into a BUILD file before the
targets themselves.

Test: soong tests
Test: TH
Test: GENERATE_BAZEL_FILES=true m nothing && build/bazel/scripts/bp2build-sync.sh write && bazel cquery //bionic/...
Fixes: 178531760

Test: TH
Change-Id: Ie5f793a00006eb024eaef07ddd9fde7aaefc054e
diff --git a/bp2build/build_conversion.go b/bp2build/build_conversion.go
index a7c3adb..1af1d60 100644
--- a/bp2build/build_conversion.go
+++ b/bp2build/build_conversion.go
@@ -18,6 +18,7 @@
 	"android/soong/android"
 	"fmt"
 	"reflect"
+	"strconv"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -29,8 +30,62 @@
 }
 
 type BazelTarget struct {
-	name    string
-	content string
+	name            string
+	content         string
+	ruleClass       string
+	bzlLoadLocation string
+}
+
+// IsLoadedFromStarlark determines if the BazelTarget's rule class is loaded from a .bzl file,
+// as opposed to a native rule built into Bazel.
+func (t BazelTarget) IsLoadedFromStarlark() bool {
+	return t.bzlLoadLocation != ""
+}
+
+// BazelTargets is a typedef for a slice of BazelTarget objects.
+type BazelTargets []BazelTarget
+
+// String returns the string representation of BazelTargets, without load
+// statements (use LoadStatements for that), since the targets are usually not
+// adjacent to the load statements at the top of the BUILD file.
+func (targets BazelTargets) String() string {
+	var res string
+	for i, target := range targets {
+		res += target.content
+		if i != len(targets)-1 {
+			res += "\n\n"
+		}
+	}
+	return res
+}
+
+// LoadStatements return the string representation of the sorted and deduplicated
+// Starlark rule load statements needed by a group of BazelTargets.
+func (targets BazelTargets) LoadStatements() string {
+	bzlToLoadedSymbols := map[string][]string{}
+	for _, target := range targets {
+		if target.IsLoadedFromStarlark() {
+			bzlToLoadedSymbols[target.bzlLoadLocation] =
+				append(bzlToLoadedSymbols[target.bzlLoadLocation], target.ruleClass)
+		}
+	}
+
+	var loadStatements []string
+	for bzl, ruleClasses := range bzlToLoadedSymbols {
+		loadStatement := "load(\""
+		loadStatement += bzl
+		loadStatement += "\", "
+		ruleClasses = android.SortedUniqueStrings(ruleClasses)
+		for i, ruleClass := range ruleClasses {
+			loadStatement += "\"" + ruleClass + "\""
+			if i != len(ruleClasses)-1 {
+				loadStatement += ", "
+			}
+		}
+		loadStatement += ")"
+		loadStatements = append(loadStatements, loadStatement)
+	}
+	return strings.Join(android.SortedUniqueStrings(loadStatements), "\n")
 }
 
 type bpToBuildContext interface {
@@ -104,8 +159,8 @@
 	return attributes
 }
 
-func GenerateSoongModuleTargets(ctx bpToBuildContext, codegenMode CodegenMode) map[string][]BazelTarget {
-	buildFileToTargets := make(map[string][]BazelTarget)
+func GenerateSoongModuleTargets(ctx bpToBuildContext, codegenMode CodegenMode) map[string]BazelTargets {
+	buildFileToTargets := make(map[string]BazelTargets)
 	ctx.VisitAllModules(func(m blueprint.Module) {
 		dir := ctx.ModuleDir(m)
 		var t BazelTarget
@@ -127,22 +182,44 @@
 	return buildFileToTargets
 }
 
+// Helper method to trim quotes around strings.
+func trimQuotes(s string) string {
+	if s == "" {
+		// strconv.Unquote would error out on empty strings, but this method
+		// allows them, so return the empty string directly.
+		return ""
+	}
+	ret, err := strconv.Unquote(s)
+	if err != nil {
+		// Panic the error immediately.
+		panic(fmt.Errorf("Trying to unquote '%s', but got error: %s", s, err))
+	}
+	return ret
+}
+
 func generateBazelTarget(ctx bpToBuildContext, m blueprint.Module) BazelTarget {
 	// extract the bazel attributes from the module.
 	props := getBuildProperties(ctx, m)
 
 	// extract the rule class name from the attributes. Since the string value
 	// will be string-quoted, remove the quotes here.
-	ruleClass := strings.Replace(props.Attrs["rule_class"], "\"", "", 2)
+	ruleClass := trimQuotes(props.Attrs["rule_class"])
 	// Delete it from being generated in the BUILD file.
 	delete(props.Attrs, "rule_class")
 
+	// extract the bzl_load_location, and also remove the quotes around it here.
+	bzlLoadLocation := trimQuotes(props.Attrs["bzl_load_location"])
+	// Delete it from being generated in the BUILD file.
+	delete(props.Attrs, "bzl_load_location")
+
 	// Return the Bazel target with rule class and attributes, ready to be
 	// code-generated.
 	attributes := propsToAttributes(props.Attrs)
 	targetName := targetNameForBp2Build(ctx, m)
 	return BazelTarget{
-		name: targetName,
+		name:            targetName,
+		ruleClass:       ruleClass,
+		bzlLoadLocation: bzlLoadLocation,
 		content: fmt.Sprintf(
 			bazelTarget,
 			ruleClass,