Repository: bazel-contrib/bazel-gazelle

Issue:
resolver.Imports method needs RegularFiles list to evaluate glob
This is an issue we encountered with gazelle_cc. I don't think there's an easy way to solve it yet, so I'm curious to hear what other folks think.

When Gazelle indexes the repo, it calls [`resolver.Resolver.Imports`](https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve#Resolver.Imports) on each rule in each indexed directory. Suppose we have a `cc_library` like this:

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

We'd like `Imports` to be able to return an entry for every file matched by the `glob`. That should include files in the current directory, as well as subdirectories within the same Bazel package. It should not include files excluded by the `.bazelignore` file or `# gazelle:exclude` directives.

The [`language.Language.GenerateRules`](https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/language#Language) method also considers this set of file from [`GenerateArgs.RegularFiles`](https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/language#GenerateArgs). We'd like to be able to access that set of files from `Imports`.

We can't add a parameter to `Imports` unfortunately since that would be an incompatible change to the interface. However, the `walk` package has all the information we need internally. I wonder if we should export a function like:

```
func GetDirInfo(rel string) (DirInfo, error)

type DirInfo struct {
  Subdirs, RegularFiles []string
  File *rule.File
}
```


Hints from issue comments:
@jayconrod: cc @WojciechMazur @jayconrod: Reopening due to feedback from @WojciechMazur and @jbedard in [this thread](https://github.com/bazel-contrib/bazel-gazelle/pull/2141/files#r2224125478). `GetDirInfo` needs to be able to return info for subdirectories that haven't been visited yet.

Solution patch:
diff --git a/pathtools/BUILD.bazel b/pathtools/BUILD.bazel
index bb31f8e..a429f87 100644
--- a/pathtools/BUILD.bazel
+++ b/pathtools/BUILD.bazel
@@ -11,6 +11,7 @@ go_test(
     name = "pathtools_test",
     srcs = ["path_test.go"],
     embed = [":pathtools"],
+    deps = ["@com_github_google_go_cmp//cmp"],
 )
 
 filegroup(
diff --git a/pathtools/path.go b/pathtools/path.go
index 36eb993..17700b5 100644
--- a/pathtools/path.go
+++ b/pathtools/path.go
@@ -114,6 +114,39 @@ func Index(p, sub string) int {
 	}
 }
 
+// Prefixes returns an iterator (iter.Seq) over all the prefixes of p.
+// For example, if p is "a/b/c", the iterator yields "", "a", "a/b", "a/b/c".
+// p must be a slash-separated path. It may be relative or absolute.
+func Prefixes(p string) func(yield func(string) bool) {
+	return func(yield func(string) bool) {
+		var slash int
+		if strings.HasPrefix(p, "/") {
+			slash = 0
+		} else {
+			slash = -1
+		}
+		if ok := yield(p[:slash+1]); !ok {
+			return
+		}
+		for {
+			i := strings.Index(p[slash+1:], "/")
+			if i < 0 {
+				break
+			}
+			if ok := yield(p[:slash+1+i]); !ok {
+				return
+			}
+			slash += 1 + i
+			for slash+1 < len(p) && p[slash+1] == '/' {
+				slash++ // skip over multiple slashes
+			}
+		}
+		if p != "" && !strings.HasSuffix(p, "/") {
+			yield(p)
+		}
+	}
+}
+
 func trimTrailingSlash(p string) string {
 	for len(p) > 1 && p[len(p)-1] == '/' {
 		p = p[:len(p)-1]
diff --git a/walk/BUILD.bazel b/walk/BUILD.bazel
index 003915b..c1f1819 100644
--- a/walk/BUILD.bazel
+++ b/walk/BUILD.bazel
@@ -13,6 +13,7 @@ go_library(
     deps = [
         "//config",
         "//flag",
+        "//pathtools",
         "//rule",
         "@com_github_bazelbuild_buildtools//build",
         "@com_github_bmatcuk_doublestar_v4//:doublestar",
diff --git a/walk/walk.go b/walk/walk.go
index 4224103..5dd10c3 100644
--- a/walk/walk.go
+++ b/walk/walk.go
@@ -27,6 +27,7 @@ import (
 	"strings"
 
 	"github.com/bazelbuild/bazel-gazelle/config"
+	"github.com/bazelbuild/bazel-gazelle/pathtools"
 	"github.com/bazelbuild/bazel-gazelle/rule"
 )
 
@@ -229,17 +230,7 @@ func Walk2(c *config.Config, cexts []config.Configurer, dirs []string, mode Mode
 
 		// Make sure to visit prefixes of relToVisit as well so we apply
 		// configuration directives.
-		slash := -1
-		for {
-			var rel string
-			i := strings.Index(relToVisit[slash+1:], "/")
-			if i < 0 {
-				rel = relToVisit
-			} else {
-				rel = relToVisit[:slash+1+i]
-				slash = slash + 1 + i
-			}
-
+		pathtools.Prefixes(relToVisit)(func(rel string) bool {
 			if v, ok := w.visits[rel]; !ok {
 				var c *config.Config
 				if ok {
@@ -253,27 +244,24 @@ func Walk2(c *config.Config, cexts []config.Configurer, dirs []string, mode Mode
 					}
 					parentCfg := w.visits[parentRel].c
 					if getWalkConfig(parentCfg).isExcludedDir(rel) {
-						break
+						return false
 					}
 					if _, err := w.cache.get(rel, w.loadDirInfo); err != nil {
 						// Error loading directory. Most commonly, this is because the
 						// directory doesn't exist, but it could actually be a file
 						// or we don't have permission, or some other I/O error.
 						// Skip it.
-						break
+						return false
 					}
 					c = parentCfg.Clone()
 				}
 				w.visit(c, rel, false)
 				if c.Strict && len(w.errs) > 0 {
-					return errors.Join(w.errs...)
+					return false
 				}
 			}
-
-			if rel == relToVisit {
-				break
-			}
-		}
+			return true
+		})
 	}
 	return errors.Join(w.errs...)
 }



Test patch:
diff --git a/pathtools/path_test.go b/pathtools/path_test.go
index c94a4c3..fd330d7 100644
--- a/pathtools/path_test.go
+++ b/pathtools/path_test.go
@@ -15,7 +15,11 @@ limitations under the License.
 
 package pathtools
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
 
 func TestHasPrefix(t *testing.T) {
 	for _, tc := range []struct {
@@ -110,3 +114,57 @@ func TestTrimPrefix(t *testing.T) {
 		})
 	}
 }
+
+func TestPrefixes(t *testing.T) {
+	for _, test := range []struct {
+		name, path string
+		want       []string
+	}{
+		{
+			name: "empty",
+			path: "",
+			want: []string{""},
+		},
+		{
+			name: "slash",
+			path: "/",
+			want: []string{"/"},
+		},
+		{
+			name: "simple_rel",
+			path: "a",
+			want: []string{"", "a"},
+		},
+		{
+			name: "simple_abs",
+			path: "/a",
+			want: []string{"/", "/a"},
+		},
+		{
+			name: "multiple_rel",
+			path: "aa/bb/cc",
+			want: []string{"", "aa", "aa/bb", "aa/bb/cc"},
+		},
+		{
+			name: "multiple_abs",
+			path: "/aa/bb/cc",
+			want: []string{"/", "/aa", "/aa/bb", "/aa/bb/cc"},
+		},
+		{
+			name: "unclean",
+			path: "a/../b//c/",
+			want: []string{"", "a", "a/..", "a/../b", "a/../b//c"},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			var got []string
+			Prefixes(test.path)(func(prefix string) bool {
+				got = append(got, prefix)
+				return true
+			})
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("bad prefixes (-want, +got): %s", diff)
+			}
+		})
+	}
+}