diff --git a/buildozer/README.md b/buildozer/README.md index ab01311ba..cb55a5eb8 100644 --- a/buildozer/README.md +++ b/buildozer/README.md @@ -108,6 +108,8 @@ Buildozer supports the following commands(`'command args'`): * `new [(before|after) ]`: Add a new rule at the end of the BUILD file (before/after ``). The identifier `__pkg__` can be used to position rules relative to package(). + * `new_if_absent [(before|after) ]`: + Same as `new`, but does nothing if a rule with the same name already exists. * `print ` * `remove `: Removes attribute `attr`. The wildcard `*` matches all attributes except `name`. diff --git a/buildozer/buildozer_test.sh b/buildozer/buildozer_test.sh index aba79c2db..cee5716e3 100755 --- a/buildozer/buildozer_test.sh +++ b/buildozer/buildozer_test.sh @@ -1306,6 +1306,20 @@ function test_new_already_exists() { assert_err "rule 'a' already exists" } +function test_new_if_absent_cc_library() { + in='cc_test(name = "a")' + run "$in" 'new_if_absent cc_library foo' '//pkg:__pkg__' + assert_equals 'cc_test(name = "a") + +cc_library(name = "foo")' +} + +function test_new_if_absent_existing_rule() { + in='cc_library(name = "foo")' + ERROR=3 run "$in" 'new_if_absent cc_library foo' '//pkg:__pkg__' + assert_equals 'cc_library(name = "foo")' +} + function test_new_before_first() { in='cc_test(name = "a")' run "$in" 'new java_library foo before a' 'pkg/BUILD' diff --git a/edit/buildozer.go b/edit/buildozer.go index b4ae48794..65e7ac2db 100644 --- a/edit/buildozer.go +++ b/edit/buildozer.go @@ -236,7 +236,9 @@ func cmdMove(opts *Options, env CmdEnvironment) (*build.File, error) { return nil, nil } -func cmdNew(opts *Options, env CmdEnvironment) (*build.File, error) { +// createNewRule is a helper function for creating a new rule. +// It is used by cmdNew and cmdNewIfAbsent. +func createNewRule(env CmdEnvironment) (*build.File, error) { kind := env.Args[0] name := env.Args[1] addAtEOF, insertionIndex, err := findInsertionIndex(env) @@ -244,10 +246,6 @@ func cmdNew(opts *Options, env CmdEnvironment) (*build.File, error) { return nil, err } - if FindRuleByName(env.File, name) != nil { - return nil, fmt.Errorf("rule '%s' already exists", name) - } - call := &build.CallExpr{X: &build.Ident{Name: kind}} rule := &build.Rule{Call: call, ImplicitName: ""} rule.SetAttr("name", &build.StringExpr{Value: name}) @@ -260,6 +258,22 @@ func cmdNew(opts *Options, env CmdEnvironment) (*build.File, error) { return env.File, nil } +func cmdNew(opts *Options, env CmdEnvironment) (*build.File, error) { + name := env.Args[1] + if FindRuleByName(env.File, name) != nil { + return nil, fmt.Errorf("rule '%s' already exists", name) + } + return createNewRule(env) +} + +func cmdNewIfAbsent(opts *Options, env CmdEnvironment) (*build.File, error) { + name := env.Args[1] + if FindRuleByName(env.File, name) != nil { + return nil, nil + } + return createNewRule(env) +} + // findInsertionIndex is used by cmdNew to find the place at which to insert the new rule. func findInsertionIndex(env CmdEnvironment) (bool, int, error) { if len(env.Args) < 4 { @@ -928,6 +942,7 @@ var AllCommands = map[string]CommandInfo{ "fix": {cmdFix, true, 0, -1, "?"}, "move": {cmdMove, true, 3, -1, " "}, "new": {cmdNew, false, 2, 4, " [(before|after) ]"}, + "new_if_absent": {cmdNewIfAbsent, false, 2, 4, " [(before|after) ]"}, "print": {cmdPrint, true, 0, -1, ""}, "remove": {cmdRemove, true, 1, -1, " "}, "remove_comment": {cmdRemoveComment, true, 0, 2, "? ?"}, diff --git a/edit/buildozer_test.go b/edit/buildozer_test.go index c1814a429..bd175c270 100644 --- a/edit/buildozer_test.go +++ b/edit/buildozer_test.go @@ -606,6 +606,54 @@ func TestCmdSubstitute(t *testing.T) { } } +var newIfAbsentTests = []struct { + args []string + buildFile string + expected string +}{ + {[]string{"go_library", "new_rule"}, + ``, + `go_library(name = "new_rule")`, + }, + {[]string{"go_library", "new_rule"}, + `go_library(name = "existing_rule")`, + `go_library(name = "existing_rule") + +go_library(name = "new_rule")`, + }, + {[]string{"go_library", "existing_rule"}, + `go_library(name = "existing_rule")`, + `go_library(name = "existing_rule")`, + }, +} + +func TestCmdNewIfAbsent(t *testing.T) { + for i, tt := range newIfAbsentTests { + bld, err := build.Parse("BUILD", []byte(tt.buildFile)) + if err != nil { + t.Error(err) + continue + } + expectedBld, err := build.Parse("BUILD", []byte(tt.expected)) + if err != nil { + t.Error(err) + continue + } + expected := strings.TrimSpace(string(build.Format(expectedBld))) + env := CmdEnvironment{ + File: bld, + Args: tt.args, + } + if result, _ := cmdNewIfAbsent(NewOpts(), env); result != nil { + bld = result + } + got := strings.TrimSpace(string(build.Format(bld))) + if got != expected { + t.Errorf("cmdNewIfAbsent(%d):\ngot:\n%s\nexpected:\n%s", i, got, expected) + } + } +} + func TestCmdDictAddSet_missingColon(t *testing.T) { for _, tc := range []struct { name string