Introduce a tool to parse and update dependencies in WORKSPACE.bazel file

There is quite a bit of work involved in updating the external dependencies used by QUICHE that can be automated.  The depstool tool will help automate some of those.

It's written in Go because that is what all of the Bazel automated parsing/editing libraries are written in.

PiperOrigin-RevId: 481194972
diff --git a/depstool/deps/parse.go b/depstool/deps/parse.go
new file mode 100644
index 0000000..238837b
--- /dev/null
+++ b/depstool/deps/parse.go
@@ -0,0 +1,123 @@
+// Package deps package provides methods to extract and manipulate external code dependencies from the QUICHE WORKSPACE.bazel file.
+package deps
+
+import (
+	"fmt"
+	"regexp"
+
+	"github.com/bazelbuild/buildtools/build"
+)
+
+var lastUpdatedRE = regexp.MustCompile(`Last updated (\d{4}-\d{2}-\d{2})`)
+
+// Entry is a parsed representation of a dependency entry in the WORKSPACE.bazel file.
+type Entry struct {
+	Name        string
+	SHA256      string
+	Prefix      string
+	URL         string
+	LastUpdated string
+}
+
+// HTTPArchiveRule returns a CallExpr describing the provided http_archive
+// rule, or nil if the expr in question is not an http_archive rule.
+func HTTPArchiveRule(expr build.Expr) (*build.CallExpr, bool) {
+	callexpr, ok := expr.(*build.CallExpr)
+	if !ok {
+		return nil, false
+	}
+	name, ok := callexpr.X.(*build.Ident)
+	if !ok || name.Name != "http_archive" {
+		return nil, false
+	}
+	return callexpr, true
+}
+
+func parseString(expr build.Expr) (string, error) {
+	str, ok := expr.(*build.StringExpr)
+	if !ok {
+		return "", fmt.Errorf("expected string as the function argument")
+	}
+	return str.Value, nil
+}
+
+func parseSingleElementList(expr build.Expr) (string, error) {
+	list, ok := expr.(*build.ListExpr)
+	if !ok {
+		return "", fmt.Errorf("expected a list as the function argument")
+	}
+	if len(list.List) != 1 {
+		return "", fmt.Errorf("expected a single-element list as the function argument, got %d elements", len(list.List))
+	}
+	return parseString(list.List[0])
+}
+
+// ParseHTTPArchiveRule parses the provided http_archive rule and returns all of the dependency metadata embedded.
+func ParseHTTPArchiveRule(callexpr *build.CallExpr) (*Entry, error) {
+	result := Entry{}
+	for _, arg := range callexpr.List {
+		assign, ok := arg.(*build.AssignExpr)
+		if !ok {
+			return nil, fmt.Errorf("a non-named argument passed as a function parameter")
+		}
+		argname, _ := build.GetParamName(assign.LHS)
+		var err error = nil
+		switch argname {
+		case "name":
+			result.Name, err = parseString(assign.RHS)
+		case "sha256":
+			result.SHA256, err = parseString(assign.RHS)
+
+			if len(assign.Comments.Suffix) != 1 {
+				return nil, fmt.Errorf("missing the \"Last updated\" comment on the sha256 field")
+			}
+			comment := assign.Comments.Suffix[0].Token
+			match := lastUpdatedRE.FindStringSubmatch(comment)
+			if match == nil {
+				return nil, fmt.Errorf("unable to parse the \"Last updated\" comment, comment value: %s", comment)
+			}
+			result.LastUpdated = match[1]
+		case "strip_prefix":
+			result.Prefix, err = parseString(assign.RHS)
+		case "urls":
+			result.URL, err = parseSingleElementList(assign.RHS)
+		default:
+			continue
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+	if result.Name == "" {
+		return nil, fmt.Errorf("missing the name field")
+	}
+	if result.SHA256 == "" {
+		return nil, fmt.Errorf("missing the sha256 field")
+	}
+	if result.URL == "" {
+		return nil, fmt.Errorf("missing the urls field")
+	}
+	return &result, nil
+}
+
+// ParseHTTPArchiveRules parses the entire WORKSPACE.bazel file and returns all of the http_archive rules in it.
+func ParseHTTPArchiveRules(source []byte) ([]*Entry, error) {
+	file, err := build.ParseWorkspace("WORKSPACE.bazel", source)
+	if err != nil {
+		return []*Entry{}, err
+	}
+
+	result := make([]*Entry, 0)
+	for _, expr := range file.Stmt {
+		callexpr, ok := HTTPArchiveRule(expr)
+		if !ok {
+			continue
+		}
+		parsed, err := ParseHTTPArchiveRule(callexpr)
+		if err != nil {
+			return []*Entry{}, err
+		}
+		result = append(result, parsed)
+	}
+	return result, nil
+}
diff --git a/depstool/deps/parse_test.go b/depstool/deps/parse_test.go
new file mode 100644
index 0000000..2a0ac1d
--- /dev/null
+++ b/depstool/deps/parse_test.go
@@ -0,0 +1,103 @@
+package deps
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/bazelbuild/buildtools/build"
+)
+
+func TestRuleParser(t *testing.T) {
+	exampleRule := `
+http_archive(
+    name = "com_google_absl",
+    sha256 = "44634eae586a7158dceedda7d8fd5cec6d1ebae08c83399f75dd9ce76324de40",  # Last updated 2022-05-18
+    strip_prefix = "abseil-cpp-3e04aade4e7a53aebbbed1a1268117f1f522bfb0",
+    urls = ["https://github.com/abseil/abseil-cpp/archive/3e04aade4e7a53aebbbed1a1268117f1f522bfb0.zip"],
+)`
+
+	file, err := build.ParseWorkspace("WORKSPACE.bazel", []byte(exampleRule))
+	if err != nil {
+		t.Fatal(err)
+	}
+	rule, ok := HTTPArchiveRule(file.Stmt[0])
+	if !ok {
+		t.Fatal("The first rule encountered is not http_archive")
+	}
+
+	deps, err := ParseHTTPArchiveRule(rule)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := Entry{
+		Name:        "com_google_absl",
+		SHA256:      "44634eae586a7158dceedda7d8fd5cec6d1ebae08c83399f75dd9ce76324de40",
+		Prefix:      "abseil-cpp-3e04aade4e7a53aebbbed1a1268117f1f522bfb0",
+		URL:         "https://github.com/abseil/abseil-cpp/archive/3e04aade4e7a53aebbbed1a1268117f1f522bfb0.zip",
+		LastUpdated: "2022-05-18",
+	}
+	if !reflect.DeepEqual(*deps, expected) {
+		t.Errorf("Parsing returned incorret result, expected:\n  %v\n, got:\n  %v", expected, *deps)
+	}
+}
+
+func TestMultipleRules(t *testing.T) {
+	exampleRules := `
+http_archive(
+    name = "com_google_absl",
+    sha256 = "44634eae586a7158dceedda7d8fd5cec6d1ebae08c83399f75dd9ce76324de40",  # Last updated 2022-05-18
+    strip_prefix = "abseil-cpp-3e04aade4e7a53aebbbed1a1268117f1f522bfb0",
+    urls = ["https://github.com/abseil/abseil-cpp/archive/3e04aade4e7a53aebbbed1a1268117f1f522bfb0.zip"],
+)
+
+irrelevant_call()
+
+http_archive(
+    name = "com_google_protobuf",
+    sha256 = "8b28fdd45bab62d15db232ec404248901842e5340299a57765e48abe8a80d930",  # Last updated 2022-05-18
+    strip_prefix = "protobuf-3.20.1",
+    urls = ["https://github.com/protocolbuffers/protobuf/archive/refs/tags/v3.20.1.tar.gz"],
+)
+`
+
+	rules, err := ParseHTTPArchiveRules([]byte(exampleRules))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(rules) != 2 {
+		t.Fatalf("Expected 2 rules, got %d", len(rules))
+	}
+	if rules[0].Name != "com_google_absl" || rules[1].Name != "com_google_protobuf" {
+		t.Errorf("Expected the two rules to be com_google_absl and com_google_protobuf, got %s and %s", rules[0].Name, rules[1].Name)
+	}
+}
+
+func TestBazelParseError(t *testing.T) {
+	exampleRule := `
+http_archive(
+    name = "com_google_absl",
+    sha256 = "44634eae586a7158dceedda7d8fd5cec6d1ebae08c83399f75dd9ce76324de40",  # Last updated 2022-05-18
+    strip_prefix = "abseil-cpp-3e04aade4e7a53aebbbed1a1268117f1f522bfb0",
+    urls = ["https://github.com/abseil/abseil-cpp/archive/3e04aade4e7a53aebbbed1a1268117f1f522bfb0.zip"],
+`
+
+	_, err := ParseHTTPArchiveRules([]byte(exampleRule))
+	if err == nil {
+		t.Errorf("Expected parser error")
+	}
+}
+
+func TestMissingField(t *testing.T) {
+	exampleRule := `
+http_archive(
+    name = "com_google_absl",
+    strip_prefix = "abseil-cpp-3e04aade4e7a53aebbbed1a1268117f1f522bfb0",
+    urls = ["https://github.com/abseil/abseil-cpp/archive/3e04aade4e7a53aebbbed1a1268117f1f522bfb0.zip"],
+)`
+
+	_, err := ParseHTTPArchiveRules([]byte(exampleRule))
+	if err == nil || err.Error() != "missing the sha256 field" {
+		t.Errorf("Expected the missing sha256 error, got %v", err)
+	}
+}
diff --git a/depstool/go.mod b/depstool/go.mod
new file mode 100644
index 0000000..6277e25
--- /dev/null
+++ b/depstool/go.mod
@@ -0,0 +1,7 @@
+module quiche.googlesource.com/quiche/depstool
+
+go 1.20
+
+require (
+	github.com/bazelbuild/buildtools v0.0.0-20221004120235-7186f635531b
+)
diff --git a/depstool/go.sum b/depstool/go.sum
new file mode 100644
index 0000000..781c5f2
--- /dev/null
+++ b/depstool/go.sum
@@ -0,0 +1,2 @@
+github.com/bazelbuild/buildtools v0.0.0-20221004120235-7186f635531b h1:jhiMzJ+8unnLRtV8rpbWBFE9pFNzIqgUTyZU5aA++w8=
+github.com/bazelbuild/buildtools v0.0.0-20221004120235-7186f635531b/go.mod h1:689QdV3hBP7Vo9dJMmzhoYIyo/9iMhEmHkJcnaPRCbo=