Skip to content

Commit 949021f

Browse files
committed
Implement mapping replacement logic for use_repo_add
When using use_repo_add, the behavior now depends on what is being added: - Adding a positional argument (e.g., "foo") that already exists as a mapping (e.g., my_foo = "foo") will preserve the existing mapping instead of adding a duplicate or converting it to positional. - Adding a mapping (e.g., bar=baz) that conflicts with an existing mapping will replace it: - Same key, different value: foo=bar + foo=baz -> foo=baz - Different key, same value: foo=bar + qux=bar -> qux=bar This allows users to explicitly rename or remap repositories while preserving user-defined mappings when they're not explicitly changed.
1 parent b28872b commit 949021f

2 files changed

Lines changed: 154 additions & 18 deletions

File tree

edit/bzlmod/bzlmod.go

Lines changed: 128 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func NewUseRepo(f *build.File, proxies []string) (*build.File, *build.CallExpr)
125125
// useRepos must not be empty.
126126
// Keyword arguments are preserved and can be added using the syntax "key=value".
127127
// For invalid identifiers (e.g., containing dots), dict unpacking syntax is used.
128+
// If a mapping conflicts with an existing one (same key or same value), the existing one is replaced.
128129
func AddRepoUsages(useRepos []*build.CallExpr, repos ...string) {
129130
if len(repos) == 0 {
130131
return
@@ -133,43 +134,153 @@ func AddRepoUsages(useRepos []*build.CallExpr, repos ...string) {
133134
panic("useRepos must not be empty")
134135
}
135136

136-
seen := make(map[string]struct{})
137+
// Parse all repos to add and track which keys/values are being added
138+
type repoToAdd struct {
139+
key string
140+
value string
141+
isMapping bool
142+
isValidIdent bool
143+
isDictUnpacking bool
144+
}
145+
146+
var reposToAdd []repoToAdd
147+
mappingValuesToAdd := make(map[string]struct{}) // Values being added as mappings
148+
positionalValuesToAdd := make(map[string]struct{}) // Values being added as positional
149+
keysToAdd := make(map[string]struct{})
150+
151+
for _, repo := range repos {
152+
key, value, isMapping := parseRepoMapping(repo)
153+
validIdent := isValidIdentifier(key)
154+
155+
reposToAdd = append(reposToAdd, repoToAdd{
156+
key: key,
157+
value: value,
158+
isMapping: isMapping,
159+
isValidIdent: validIdent,
160+
isDictUnpacking: isMapping && !validIdent,
161+
})
162+
163+
if isMapping {
164+
mappingValuesToAdd[value] = struct{}{}
165+
keysToAdd[key] = struct{}{}
166+
} else {
167+
positionalValuesToAdd[value] = struct{}{}
168+
}
169+
}
170+
171+
// Track existing values to detect duplicates when adding positional args
172+
existingValues := make(map[string]struct{})
137173
for _, useRepo := range useRepos {
138174
if len(useRepo.List) == 0 {
139-
// Invalid use_repo call, skip.
140175
continue
141176
}
142177
for _, arg := range useRepo.List[1:] {
143-
seen[repoFromUseRepoArg(arg)] = struct{}{}
178+
if val := repoFromUseRepoArg(arg); val != "" {
179+
existingValues[val] = struct{}{}
180+
}
144181
}
145182
}
146183

147-
lastUseRepo := getLastUseRepo(useRepos)
148-
var dictEntries []*build.KeyValueExpr
184+
// Remove conflicting arguments from all use_repo calls
185+
for _, useRepo := range useRepos {
186+
if len(useRepo.List) == 0 {
187+
continue
188+
}
149189

150-
for _, repo := range repos {
151-
key, value, isMapping := parseRepoMapping(repo)
190+
var newArgs []build.Expr
191+
var dictToUpdate *build.DictExpr
152192

153-
// Check if already exists
154-
if _, ok := seen[value]; ok {
155-
continue
193+
for _, arg := range useRepo.List[1:] {
194+
shouldKeep := true
195+
196+
switch arg := arg.(type) {
197+
case *build.StringExpr:
198+
// Positional argument: only remove if a mapping is being added with this value
199+
if _, conflicts := mappingValuesToAdd[arg.Value]; conflicts {
200+
shouldKeep = false
201+
}
202+
case *build.AssignExpr:
203+
// Keyword argument: remove if same key is being added
204+
if ident, ok := arg.LHS.(*build.Ident); ok {
205+
if _, conflicts := keysToAdd[ident.Name]; conflicts {
206+
shouldKeep = false
207+
}
208+
}
209+
// Or if same value is being added as a mapping (replacement)
210+
if str, ok := arg.RHS.(*build.StringExpr); ok {
211+
if _, conflicts := mappingValuesToAdd[str.Value]; conflicts {
212+
shouldKeep = false
213+
}
214+
}
215+
case *build.UnaryExpr:
216+
// Dict unpacking: we'll handle this separately
217+
if arg.Op == "**" {
218+
if dict, ok := arg.X.(*build.DictExpr); ok {
219+
dictToUpdate = dict
220+
shouldKeep = false // Remove from list, we'll re-add after filtering
221+
}
222+
}
223+
}
224+
225+
if shouldKeep {
226+
newArgs = append(newArgs, arg)
227+
}
156228
}
157229

158-
if !isMapping {
230+
// Filter dict entries if we have a dict to update
231+
if dictToUpdate != nil {
232+
var filteredDictEntries []*build.KeyValueExpr
233+
for _, kv := range dictToUpdate.List {
234+
shouldKeep := true
235+
// Check if key conflicts
236+
if keyStr, ok := kv.Key.(*build.StringExpr); ok {
237+
if _, conflicts := keysToAdd[keyStr.Value]; conflicts {
238+
shouldKeep = false
239+
}
240+
}
241+
// Check if value conflicts with a mapping being added
242+
if valStr, ok := kv.Value.(*build.StringExpr); ok {
243+
if _, conflicts := mappingValuesToAdd[valStr.Value]; conflicts {
244+
shouldKeep = false
245+
}
246+
}
247+
if shouldKeep {
248+
filteredDictEntries = append(filteredDictEntries, kv)
249+
}
250+
}
251+
// Only keep dict if it still has entries after filtering
252+
if len(filteredDictEntries) > 0 {
253+
dictToUpdate.List = filteredDictEntries
254+
newArgs = append(newArgs, &build.UnaryExpr{Op: "**", X: dictToUpdate})
255+
}
256+
}
257+
258+
useRepo.List = append(useRepo.List[:1], newArgs...)
259+
}
260+
261+
// Add new repos to the last use_repo call
262+
lastUseRepo := getLastUseRepo(useRepos)
263+
var dictEntries []*build.KeyValueExpr
264+
265+
for _, repo := range reposToAdd {
266+
if !repo.isMapping {
159267
// Simple positional argument: use_repo(ext, "foo")
160-
lastUseRepo.List = append(lastUseRepo.List, &build.StringExpr{Value: repo})
161-
} else if isValidIdentifier(key) {
268+
// Skip if already exists (to preserve existing mappings)
269+
if _, exists := existingValues[repo.value]; !exists {
270+
lastUseRepo.List = append(lastUseRepo.List, &build.StringExpr{Value: repo.value})
271+
}
272+
} else if repo.isValidIdent {
162273
// Valid identifier as keyword argument: use_repo(ext, bar = "baz")
163274
lastUseRepo.List = append(lastUseRepo.List, &build.AssignExpr{
164-
LHS: &build.Ident{Name: key},
275+
LHS: &build.Ident{Name: repo.key},
165276
Op: "=",
166-
RHS: &build.StringExpr{Value: value},
277+
RHS: &build.StringExpr{Value: repo.value},
167278
})
168279
} else {
169280
// Invalid identifier, need dict unpacking: use_repo(ext, **{"foo.2": "foo"})
170281
dictEntries = append(dictEntries, &build.KeyValueExpr{
171-
Key: &build.StringExpr{Value: key},
172-
Value: &build.StringExpr{Value: value},
282+
Key: &build.StringExpr{Value: repo.key},
283+
Value: &build.StringExpr{Value: repo.value},
173284
})
174285
}
175286
}

edit/bzlmod/bzlmod_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ use_repo(prox, "repo3", "repo4")
502502
{
503503
`use_repo(prox, my_repo = "actual")`,
504504
[]string{"my_other=actual", "new=other"},
505-
`use_repo(prox, my_repo = "actual", new = "other")
505+
`use_repo(prox, my_other = "actual", new = "other")
506506
`,
507507
},
508508
// Test that existing mappings are preserved when adding the same underlying repo
@@ -522,6 +522,31 @@ use_repo(prox, "repo3", "repo4")
522522
`use_repo(ext, my_mapping = "actual_repo", "existing")`,
523523
[]string{"actual_repo", "new_repo"},
524524
`use_repo(ext, "existing", "new_repo", my_mapping = "actual_repo")
525+
`,
526+
},
527+
// Test that mappings can be replaced/renamed
528+
{
529+
`use_repo(proxy, foo = "bar")`,
530+
[]string{"foo=baz"},
531+
`use_repo(proxy, foo = "baz")
532+
`,
533+
},
534+
{
535+
`use_repo(proxy, foo = "bar")`,
536+
[]string{"qux=bar"},
537+
`use_repo(proxy, qux = "bar")
538+
`,
539+
},
540+
{
541+
`use_repo(proxy, foo = "bar", "other")`,
542+
[]string{"foo=baz"},
543+
`use_repo(proxy, "other", foo = "baz")
544+
`,
545+
},
546+
{
547+
`use_repo(proxy, foo = "bar", "other")`,
548+
[]string{"qux=bar", "new"},
549+
`use_repo(proxy, "other", "new", qux = "bar")
525550
`,
526551
},
527552
} {

0 commit comments

Comments
 (0)