Skip to content

Commit 79ae18f

Browse files
committed
feat(pkg): scope queries to incremental dirty targets
1 parent 6d3b05f commit 79ae18f

3 files changed

Lines changed: 173 additions & 0 deletions

File tree

pkg/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ go_library(
1111
"hash_cache.go",
1212
"hash_persistence.go",
1313
"normalizer.go",
14+
"scoped_query.go",
1415
"target_determinator.go",
1516
"targets_list.go",
1617
"walker.go",
@@ -41,6 +42,7 @@ go_test(
4142
"hash_cache_test.go",
4243
"hash_persistence_test.go",
4344
"normalizer_test.go",
45+
"scoped_query_test.go",
4446
"seed_hashes_test.go",
4547
"target_determinator_test.go",
4648
"walker_test.go",

pkg/scoped_query.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package pkg
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strings"
7+
)
8+
9+
// BuildScopedUniverse returns a bazel query expression covering the scoped
10+
// universe of a seeded run:
11+
//
12+
// - each dirty package contributes a `//pkg:all` wildcard, so that targets
13+
// added to or removed from the package since the seed are picked up
14+
// (explicit labels from the seed would miss new targets and error on
15+
// deleted ones);
16+
// - carried labels — dirty* targets in unchanged packages (i.e. reverse
17+
// deps of the dirty packages) — are named explicitly via set(). These
18+
// labels are guaranteed to still exist because their BUILD files did not
19+
// change and macro (.bzl) changes trigger a full-rehash fallback.
20+
//
21+
// The returned expression is parenthesized so it can be substituted into a
22+
// larger query expression.
23+
func BuildScopedUniverse(dirtyPackages []string, carriedLabels []string) string {
24+
terms := make([]string, 0, len(dirtyPackages)+1)
25+
for _, pkg := range dirtyPackages {
26+
if pkg == "//" {
27+
terms = append(terms, "//:all")
28+
} else {
29+
terms = append(terms, pkg+":all")
30+
}
31+
}
32+
if len(carriedLabels) > 0 {
33+
sorted := append([]string(nil), carriedLabels...)
34+
sort.Strings(sorted)
35+
terms = append(terms, "set("+strings.Join(sorted, " ")+")")
36+
}
37+
if len(terms) == 0 {
38+
return "set()"
39+
}
40+
return "(" + strings.Join(terms, " + ") + ")"
41+
}
42+
43+
// ScopeTargetsPattern narrows a targets pattern built over //... to the given
44+
// universe expression by substituting every occurrence of //... with the
45+
// universe. This preserves the semantics of filters expressed over the full
46+
// repo — e.g. `//... - attr(tags, "manual", //...)` becomes
47+
// `(U) - attr(tags, "manual", (U))` — while restricting evaluation to the
48+
// scoped universe, so manual-tag filtering behaves identically to a full run.
49+
//
50+
// Patterns that do not contain //... cannot be scoped this way; callers
51+
// should fall back to a full computation in that case.
52+
//
53+
// Known limitations of scoped mode versus a full //... query:
54+
// - --filter-incompatible-targets is a cquery-only concern; the seeded path
55+
// is only supported with --query-backend=query, which never filters
56+
// incompatible targets in full mode either.
57+
// - Targets in unchanged packages whose rule-generation depends on state
58+
// outside the package (e.g. macros) are not re-listed; any .bzl change
59+
// triggers a full-rehash fallback before reaching this point.
60+
func ScopeTargetsPattern(originalPattern string, universe string) (string, error) {
61+
var result strings.Builder
62+
replacements := 0
63+
for start := 0; start < len(originalPattern); {
64+
idx := strings.Index(originalPattern[start:], "//...")
65+
if idx < 0 {
66+
result.WriteString(originalPattern[start:])
67+
break
68+
}
69+
idx += start
70+
result.WriteString(originalPattern[start:idx])
71+
// Do not rewrite repository-qualified wildcards such as @repo//....
72+
if idx > 0 && isLabelCharacter(originalPattern[idx-1]) {
73+
result.WriteString("//...")
74+
} else {
75+
result.WriteString(universe)
76+
replacements++
77+
}
78+
start = idx + len("//...")
79+
}
80+
if replacements == 0 {
81+
return "", fmt.Errorf("targets pattern %q does not contain //... and cannot be scoped", originalPattern)
82+
}
83+
return result.String(), nil
84+
}
85+
86+
func isLabelCharacter(c byte) bool {
87+
return c == '@' || c == '_' || c == '-' || c == '.' || c == '+' ||
88+
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
89+
}

pkg/scoped_query_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package pkg
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestBuildScopedUniverseEmpty(t *testing.T) {
8+
got := BuildScopedUniverse(nil, nil)
9+
if got != "set()" {
10+
t.Errorf("empty universe: want %q, got %q", "set()", got)
11+
}
12+
}
13+
14+
func TestBuildScopedUniversePackagesOnly(t *testing.T) {
15+
got := BuildScopedUniverse([]string{"//a", "//b/c"}, nil)
16+
want := "(//a:all + //b/c:all)"
17+
if got != want {
18+
t.Errorf("want %q, got %q", want, got)
19+
}
20+
}
21+
22+
func TestBuildScopedUniverseRootPackage(t *testing.T) {
23+
got := BuildScopedUniverse([]string{"//"}, nil)
24+
if got != "(//:all)" {
25+
t.Fatalf("BuildScopedUniverse(root) = %q, want %q", got, "(//:all)")
26+
}
27+
}
28+
29+
func TestBuildScopedUniverseLabelsOnly(t *testing.T) {
30+
got := BuildScopedUniverse(nil, []string{"//z:z", "//a:a"})
31+
want := "(set(//a:a //z:z))"
32+
if got != want {
33+
t.Errorf("want %q, got %q", want, got)
34+
}
35+
}
36+
37+
func TestBuildScopedUniversePackagesAndLabels(t *testing.T) {
38+
got := BuildScopedUniverse([]string{"//pkg"}, []string{"//app:binary"})
39+
want := "(//pkg:all + set(//app:binary))"
40+
if got != want {
41+
t.Errorf("want %q, got %q", want, got)
42+
}
43+
}
44+
45+
func TestScopeTargetsPatternPlain(t *testing.T) {
46+
got, err := ScopeTargetsPattern("//...", "(//pkg:all)")
47+
if err != nil {
48+
t.Fatalf("ScopeTargetsPattern: %v", err)
49+
}
50+
if got != "(//pkg:all)" {
51+
t.Errorf("want %q, got %q", "(//pkg:all)", got)
52+
}
53+
}
54+
55+
func TestScopeTargetsPatternManualFilter(t *testing.T) {
56+
original := `//... - attr(tags, "manual", //...)`
57+
got, err := ScopeTargetsPattern(original, "(//pkg:all + set(//app:binary))")
58+
if err != nil {
59+
t.Fatalf("ScopeTargetsPattern: %v", err)
60+
}
61+
want := `(//pkg:all + set(//app:binary)) - attr(tags, "manual", (//pkg:all + set(//app:binary)))`
62+
if got != want {
63+
t.Errorf("want %q, got %q", want, got)
64+
}
65+
}
66+
67+
func TestScopeTargetsPatternNoWildcard(t *testing.T) {
68+
if _, err := ScopeTargetsPattern("//app:binary", "(//pkg:all)"); err == nil {
69+
t.Fatal("expected error for pattern without //...")
70+
}
71+
}
72+
73+
func TestScopeTargetsPatternDoesNotRewriteExternalWildcard(t *testing.T) {
74+
got, err := ScopeTargetsPattern("@repo//... + //...", "(//pkg:all)")
75+
if err != nil {
76+
t.Fatal(err)
77+
}
78+
want := "@repo//... + (//pkg:all)"
79+
if got != want {
80+
t.Fatalf("ScopeTargetsPattern = %q, want %q", got, want)
81+
}
82+
}

0 commit comments

Comments
 (0)