From a0b5fe51e81a7c50999b468ff802cf2a33a7350e Mon Sep 17 00:00:00 2001 From: kisabaka <648530+kisabaka@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:25:40 +0200 Subject: [PATCH 1/2] Add command that updates existing attributes with a select --- edit/buildozer.go | 80 ++++++++++++ edit/buildozer_test.go | 274 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) diff --git a/edit/buildozer.go b/edit/buildozer.go index d246bfa65..c28a2df9e 100644 --- a/edit/buildozer.go +++ b/edit/buildozer.go @@ -702,6 +702,85 @@ func cmdSetSelect(opts *Options, env CmdEnvironment) (*build.File, error) { return env.File, nil } +// addToListSelect adds value under key in the last select() of a list attribute, +// creating a new bare select if none exists, or concatenating one if the attribute +// is already a plain list. +func addToListSelect(r *build.Rule, attr, pkg, key string, value build.Expr) { + existing := r.Attr(attr) + selects := AllSelects(existing) + + if len(selects) > 0 { + last := selects[len(selects)-1] + dict := last.List[0].(*build.DictExpr) + cur := DictionaryGet(dict, key) + if cur != nil { + DictionarySet(dict, key, AddValueToList(cur, pkg, value, false)) + } else { + DictionarySet(dict, key, &build.ListExpr{List: []build.Expr{value}}) + if DictionaryGet(dict, "//conditions:default") == nil { + DictionarySet(dict, "//conditions:default", &build.ListExpr{}) + } + } + r.SetAttr(attr, existing) + return + } + + dict := &build.DictExpr{ + List: []*build.KeyValueExpr{ + {Key: &build.StringExpr{Value: key}, Value: &build.ListExpr{List: []build.Expr{value}}}, + {Key: &build.StringExpr{Value: "//conditions:default"}, Value: &build.ListExpr{}}, + }, + } + sel := &build.CallExpr{List: []build.Expr{dict}} + sel.X = &build.Ident{Name: "select"} + if existing == nil { + r.SetAttr(attr, sel) + } else { + r.SetAttr(attr, &build.BinaryExpr{Op: "+", X: existing, Y: sel}) + } +} + +// addToScalarSelect sets value under key in the last select() of a scalar attribute, +// creating a new select if none exists. An existing key is overwritten. +func addToScalarSelect(r *build.Rule, attr, key string, value build.Expr) { + existing := r.Attr(attr) + selects := AllSelects(existing) + + if len(selects) > 0 { + last := selects[len(selects)-1] + dict := last.List[0].(*build.DictExpr) + DictionarySet(dict, key, value) + r.SetAttr(attr, existing) + return + } + dict := &build.DictExpr{ + List: []*build.KeyValueExpr{ + {Key: &build.StringExpr{Value: key}, Value: value}, + }, + } + sel := &build.CallExpr{List: []build.Expr{dict}} + sel.X = &build.Ident{Name: "select"} + r.SetAttr(attr, sel) +} + +func cmdAddSelect(opts *Options, env CmdEnvironment) (*build.File, error) { + attr := env.Args[0] + key := env.Args[1] + values := env.Args[2:] + + if IsList(attr) { + for _, val := range values { + addToListSelect(env.Rule, attr, env.Pkg, key, getStringExpr(val, env.Pkg)) + } + } else { + if len(values) != 1 { + return nil, fmt.Errorf("add_select: scalar attribute %q requires exactly one value", attr) + } + addToScalarSelect(env.Rule, attr, key, getStringExpr(values[0], env.Pkg)) + } + return env.File, nil +} + // cmdDictSet adds a key to a dict, overwriting any previous values. func cmdDictSet(opts *Options, env CmdEnvironment) (*build.File, error) { attr := env.Args[0] @@ -926,6 +1005,7 @@ type CommandInfo struct { // of arguments. var AllCommands = map[string]CommandInfo{ "add": {cmdAdd, true, 2, -1, " "}, + "add_select": {cmdAddSelect, true, 3, -1, " "}, "new_load": {cmdNewLoad, false, 1, -1, " <[to=]from(s)>"}, "replace_load": {cmdReplaceLoad, false, 1, -1, " <[to=]symbol(s)>"}, "substitute_load": {cmdSubstituteLoad, false, 2, 2, " "}, diff --git a/edit/buildozer_test.go b/edit/buildozer_test.go index 5b1780f8d..018190831 100644 --- a/edit/buildozer_test.go +++ b/edit/buildozer_test.go @@ -950,6 +950,280 @@ func TestCmdSetSelect(t *testing.T) { } } +func TestCmdAddSelect_listValues(t *testing.T) { + for _, tc := range []struct { + name string + args []string + buildFile string + expected string + }{ + { + name: "list_attr_absent", + args: []string{"deps", ":added_select_key", "//some_package:lib"}, + buildFile: `foo( + name = "foo", +)`, + expected: `foo( + name = "foo", + deps = select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + }, + { + name: "list_attr_plain_list", + args: []string{"deps", ":added_select_key", "//some_package:lib"}, + buildFile: `foo( + name = "foo", + deps = ["//base:lib"], +)`, + expected: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + }, + { + name: "list_attr_pure_select_new_key", + args: []string{"deps", ":second_select_key", "//my_path:lib"}, + buildFile: `foo( + name = "foo", + deps = select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + expected: `foo( + name = "foo", + deps = select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + ":second_select_key": ["//my_path:lib"], + }), +)`, + }, + { + name: "list_attr_pure_select_existing_key", + args: []string{"deps", ":added_select_key", "//some_package:lib2"}, + buildFile: `foo( + name = "foo", + deps = select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + expected: `foo( + name = "foo", + deps = select({ + ":added_select_key": [ + "//some_package:lib", + "//some_package:lib2", + ], + "//conditions:default": [], + }), +)`, + }, + { + name: "list_attr_list_plus_select_new_key", + args: []string{"deps", ":second_select_key", "//my_path:lib"}, + buildFile: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + expected: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + ":second_select_key": ["//my_path:lib"], + }), +)`, + }, + { + name: "list_attr_list_plus_select_existing_key", + args: []string{"deps", ":added_select_key", "//some_package:lib2"}, + buildFile: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + expected: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": [ + "//some_package:lib", + "//some_package:lib2", + ], + "//conditions:default": [], + }), +)`, + }, + { + name: "list_attr_duplicate_value_is_noop", + args: []string{"deps", ":added_select_key", "//some_package:lib"}, + buildFile: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + expected: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": [], + }), +)`, + }, + { + name: "list_attr_multiple_values", + args: []string{"deps", ":added_select_key", "//some_package:lib1", "//some_package:lib2"}, + buildFile: `foo( + name = "foo", + deps = ["//base:lib"], +)`, + expected: `foo( + name = "foo", + deps = ["//base:lib"] + select({ + ":added_select_key": [ + "//some_package:lib1", + "//some_package:lib2", + ], + "//conditions:default": [], + }), +)`, + }, + { + name: "list_attr_new_key_no_default_added_when_default_exists", + args: []string{"deps", ":second_select_key", "//my_path:lib"}, + buildFile: `foo( + name = "foo", + deps = select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": ["//fallback:lib"], + }), +)`, + expected: `foo( + name = "foo", + deps = select({ + ":added_select_key": ["//some_package:lib"], + "//conditions:default": ["//fallback:lib"], + ":second_select_key": ["//my_path:lib"], + }), +)`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + bld, err := build.Parse("BUILD", []byte(tc.buildFile)) + if err != nil { + t.Error(err) + } + rl := bld.Rules("foo")[0] + env := CmdEnvironment{ + File: bld, + Rule: rl, + Args: tc.args, + } + bld, err = cmdAddSelect(NewOpts(), env) + if err != nil { + t.Fatalf("cmdAddSelect returned err: %s", err) + } + got := strings.TrimSpace(string(build.Format(bld))) + if got != tc.expected { + t.Errorf("cmdAddSelect %v:\ngot:\n%s\nexpected:\n%s", tc.args, got, tc.expected) + } + }) + } +} + +func TestCmdAddSelect_scalarValues(t *testing.T) { + for _, tc := range []struct { + name string + args []string + buildFile string + expected string + }{ + { + name: "scalar_attr_absent", + args: []string{"deprecation", ":added_select_key", "deprecated on some_package"}, + buildFile: `foo( + name = "foo", +)`, + expected: `foo( + name = "foo", + deprecation = select({":added_select_key": "deprecated on some_package"}), +)`, + }, + { + name: "scalar_attr_pure_select_new_key", + args: []string{"deprecation", ":second_select_key", "deprecated on my_path"}, + buildFile: `foo( + name = "foo", + deprecation = select({ + ":added_select_key": "deprecated on some_package", + "//conditions:default": "not deprecated", + }), +)`, + expected: `foo( + name = "foo", + deprecation = select({ + ":added_select_key": "deprecated on some_package", + "//conditions:default": "not deprecated", + ":second_select_key": "deprecated on my_path", + }), +)`, + }, + { + name: "scalar_attr_pure_select_existing_key_overwrites", + args: []string{"deprecation", ":added_select_key", "updated message"}, + buildFile: `foo( + name = "foo", + deprecation = select({ + ":added_select_key": "old message", + "//conditions:default": "not deprecated", + }), +)`, + expected: `foo( + name = "foo", + deprecation = select({ + ":added_select_key": "updated message", + "//conditions:default": "not deprecated", + }), +)`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + bld, err := build.Parse("BUILD", []byte(tc.buildFile)) + if err != nil { + t.Error(err) + } + rl := bld.Rules("foo")[0] + env := CmdEnvironment{ + File: bld, + Rule: rl, + Args: tc.args, + } + bld, err = cmdAddSelect(NewOpts(), env) + if err != nil { + t.Fatalf("cmdAddSelect returned err: %s", err) + } + got := strings.TrimSpace(string(build.Format(bld))) + if got != tc.expected { + t.Errorf("cmdAddSelect %v:\ngot:\n%s\nexpected:\n%s", tc.args, got, tc.expected) + } + }) + } +} + func TestExecuteCommandsOnInlineFile(t *testing.T) { tests := []struct { name string From 84ad2eccf336582080091b5d6c23f24208b29496 Mon Sep 17 00:00:00 2001 From: D K <648530+kisabaka@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:02:37 +0200 Subject: [PATCH 2/2] Update edit/buildozer.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- edit/buildozer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edit/buildozer.go b/edit/buildozer.go index c28a2df9e..a77e90763 100644 --- a/edit/buildozer.go +++ b/edit/buildozer.go @@ -714,7 +714,7 @@ func addToListSelect(r *build.Rule, attr, pkg, key string, value build.Expr) { dict := last.List[0].(*build.DictExpr) cur := DictionaryGet(dict, key) if cur != nil { - DictionarySet(dict, key, AddValueToList(cur, pkg, value, false)) +DictionarySet(dict, key, AddValueToList(cur, pkg, value, !attributeMustNotBeSorted(r.Name(), attr))) } else { DictionarySet(dict, key, &build.ListExpr{List: []build.Expr{value}}) if DictionaryGet(dict, "//conditions:default") == nil {