Repository: bazel-contrib/bazel-gazelle

Issue:
rule: ability to parse glob expressions
In gazelle_cc, we're looking for a way to index `cc_library` targets with `glob` expressions like this:

```
cc_library(
    name = "foo",
    hdrs = glob(["**/*.h"]),
)
```

For now, we don't need to be able to generate or merge such expressions, but it would be nice to be able to extract them from the syntax tree without a custom pattern matcher.

There's already a [`rule.GlobValue`](https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/rule#GlobValue) type. It's used by the Go extension to generate `data = glob(["testdata/**"])`. It doesn't support merging yet (#1036).

A function like this would work for us:

```
// MatchGlob checks whether expr is a simple glob expression and returns a GlobValue and true
// if it is. MatchGlob returns the zero value and false if the expression is not a glob expression
// or if it can't be represented with GlobValue.
func MatchGlob(expr bzl.Expr) (GlobValue, bool)
```


Discussion:
@jayconrod: cc @WojciechMazur 

Solution patch:
diff --git a/rule/value.go b/rule/value.go
index 106e71e..52b59d7 100644
--- a/rule/value.go
+++ b/rule/value.go
@@ -45,17 +45,64 @@ func (g GlobValue) BzlExpr() bzl.Expr {
 	if len(g.Excludes) > 0 {
 		excludesValue := ExprFromValue(g.Excludes)
 		globArgs = append(globArgs, &bzl.AssignExpr{
-			LHS: &bzl.LiteralExpr{Token: "exclude"},
+			LHS: &bzl.Ident{Name: "exclude"},
 			Op:  "=",
 			RHS: excludesValue,
 		})
 	}
 	return &bzl.CallExpr{
-		X:    &bzl.LiteralExpr{Token: "glob"},
+		X:    &bzl.Ident{Name: "glob"},
 		List: globArgs,
 	}
 }
 
+// ParseGlobExpr detects whether the given expression is a call to the glob
+// function. If it is, ParseGlobExpr returns the glob's patterns and excludes
+// (if they are literal strings) and true. If not, ParseGlobExpr returns false.
+func ParseGlobExpr(e bzl.Expr) (GlobValue, bool) {
+	call, ok := e.(*bzl.CallExpr)
+	if !ok {
+		return GlobValue{}, false
+	}
+	callee, ok := call.X.(*bzl.Ident)
+	if !ok || callee.Name != "glob" {
+		return GlobValue{}, false
+	}
+	var glob GlobValue
+	for i, arg := range call.List {
+		list, ok := arg.(*bzl.ListExpr)
+		if i == 0 && ok {
+			glob.Patterns = make([]string, 0, len(list.List))
+			for _, e := range list.List {
+				if str, ok := e.(*bzl.StringExpr); ok {
+					glob.Patterns = append(glob.Patterns, str.Value)
+				}
+			}
+			continue
+		}
+
+		kv, ok := arg.(*bzl.AssignExpr)
+		if !ok {
+			continue
+		}
+		key, ok := kv.LHS.(*bzl.Ident)
+		if !ok || key.Name != "exclude" {
+			continue
+		}
+		list, ok = kv.RHS.(*bzl.ListExpr)
+		if !ok {
+			continue
+		}
+		glob.Excludes = make([]string, 0, len(list.List))
+		for _, e := range list.List {
+			if str, ok := e.(*bzl.StringExpr); ok {
+				glob.Excludes = append(glob.Excludes, str.Value)
+			}
+		}
+	}
+	return glob, true
+}
+
 // BzlExprValue is implemented by types that have custom translations
 // to Starlark values.
 type BzlExprValue interface {



Test patch:
diff --git a/rule/value_test.go b/rule/value_test.go
index f0071e4..fce1a38 100644
--- a/rule/value_test.go
+++ b/rule/value_test.go
@@ -18,8 +18,8 @@ package rule
 import (
 	"testing"
 
-	bzl "github.com/bazelbuild/buildtools/build"
 	"github.com/bazelbuild/bazel-gazelle/label"
+	bzl "github.com/bazelbuild/buildtools/build"
 	"github.com/google/go-cmp/cmp"
 )
 
@@ -33,7 +33,7 @@ func TestExprFromValue(t *testing.T) {
 				Patterns: []string{"a", "b"},
 			},
 			want: &bzl.CallExpr{
-				X: &bzl.LiteralExpr{Token: "glob"},
+				X: &bzl.Ident{Name: "glob"},
 				List: []bzl.Expr{
 					&bzl.ListExpr{
 						List: []bzl.Expr{
@@ -50,7 +50,7 @@ func TestExprFromValue(t *testing.T) {
 				Excludes: []string{"c", "d"},
 			},
 			want: &bzl.CallExpr{
-				X: &bzl.LiteralExpr{Token: "glob"},
+				X: &bzl.Ident{Name: "glob"},
 				List: []bzl.Expr{
 					&bzl.ListExpr{
 						List: []bzl.Expr{
@@ -59,7 +59,7 @@ func TestExprFromValue(t *testing.T) {
 						},
 					},
 					&bzl.AssignExpr{
-						LHS: &bzl.LiteralExpr{Token: "exclude"},
+						LHS: &bzl.Ident{Name: "exclude"},
 						Op:  "=",
 						RHS: &bzl.ListExpr{
 							List: []bzl.Expr{
@@ -104,3 +104,71 @@ func TestExprFromValue(t *testing.T) {
 		})
 	}
 }
+
+func TestParseGlobExpr(t *testing.T) {
+	for _, test := range []struct {
+		name, text string
+		want       GlobValue
+	}{
+		{
+			name: "empty",
+			text: "glob()",
+			want: GlobValue{},
+		},
+		{
+			name: "patterns_only",
+			text: `glob(["a", "b"])`,
+			want: GlobValue{Patterns: []string{"a", "b"}},
+		},
+		{
+			name: "excludes_only",
+			text: `glob(exclude = ["x", "y"])`,
+			want: GlobValue{Excludes: []string{"x", "y"}},
+		},
+		{
+			name: "patterns_before_excludes",
+			text: `glob(["a", "b"], exclude = ["x", "y"])`,
+			want: GlobValue{Patterns: []string{"a", "b"}, Excludes: []string{"x", "y"}},
+		},
+		{
+			name: "excludes_before_patterns",
+			text: `glob(exclude = ["x", "y"], ["a", "b"])`,
+			want: GlobValue{Excludes: []string{"x", "y"}},
+		},
+		{
+			name: "patterns_nonliteral",
+			text: `glob(["a", B])`,
+			want: GlobValue{Patterns: []string{"a"}},
+		},
+		{
+			name: "excludes_nonliteral",
+			text: `glob(exclude = ["x", Y])`,
+			want: GlobValue{Excludes: []string{"x"}},
+		},
+		{
+			name: "other_args",
+			text: `glob(["a"], allow_empty = True, exclude_directories = 1)`,
+			want: GlobValue{Patterns: []string{"a"}},
+		},
+		{
+			name: "invalid_args",
+			text: `glob(["a"], ["b"], exclude = ["x"], unknown = 1, *args, **kwargs)`,
+			want: GlobValue{Patterns: []string{"a"}, Excludes: []string{"x"}},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f, err := bzl.ParseDefault(test.name, []byte(test.text))
+			if err != nil {
+				t.Fatal(err)
+			}
+			e := f.Stmt[0]
+			glob, ok := ParseGlobExpr(e)
+			if !ok {
+				t.Fatal("not a glob expression")
+			}
+			if diff := cmp.Diff(glob, test.want); diff != "" {
+				t.Errorf("glob (-got, +want):\n%s", diff)
+			}
+		})
+	}
+}