Allow callers to explicitly provide types for set and add commands (#1448)
diff --git a/buildozer/README.md b/buildozer/README.md
index d907ff9..82a9394 100644
--- a/buildozer/README.md
+++ b/buildozer/README.md
@@ -86,8 +86,10 @@
Buildozer supports the following commands(`'command args'`):
- * `add <attr> <value(s)>`: Adds value(s) to a list attribute of a rule. If a
- value is already present in the list, it is not added.
+ * `add <attr>[:<type>] <value(s)>`: Adds value(s) to a list attribute of a rule.
+ If a value is already present in the list, it is not added. `type` specifies
+ the type of values being added. See [supported types](#supported-types) for
+ the available types.
* `new_load <path> <[to=]from(s)>`: Add a load statement for the given path,
importing the symbols. Afterwards, consider running `buildozer 'fix unusedLoads'`.
* `replace_load <path> <[to=]from(s)>`: Similar to `new_load`, but removes
@@ -130,10 +132,14 @@
be a simple replacement string, but it may also expand numbered or named
groups using `$0` or `$x`. Lists without strings that match `old_regexp`
are not modified.
- * `set <attr> <value(s)>`: Sets the value of an attribute. If the attribute
- was already present, its old value is replaced.
- * `set_if_absent <attr> <value(s)>`: Sets the value of an attribute. If the
- attribute was already present, no action is taken.
+ * `set <attr>[:<type>] <value(s)>`: Sets the value of an attribute. If the
+ attribute was already present, its old value is replaced. `type` specifies
+ the type of values being added. See
+ [supported types](#supported-types) for the available types.
+ * `set_if_absent <attr>[:<type>] <value(s)>`: Sets the value of an attribute.
+ If the attribute was already present, no action is taken. `type` specifies
+ the type of values being added. See [supported types](#supported-types) for
+ * the available types.
* `set kind <value>`: Set the target type to value.
* `set_select <attr> <key_1> <value_1> <key_n> <value_n>`
* `copy <attr> <from_rule>`: Copies the value of `attr` between rules. If it
@@ -188,6 +194,15 @@
usages with `dev_dependency = True` will be considered instead. Extension
usages with `isolated = True` are ignored.
+#### Supported types
+
+For the commands that support explicitly providing argument types, the following
+types are available:
+
+ * `expr`: arbitrary expression, won't be quoted as a string unless already
+ quoted. Buildozer doesn't check that the expression is syntactically
+ correct, however the subsequent formatting step will fail if it's not.
+
#### Examples
```bash
@@ -247,6 +262,9 @@
# Add an attribute new_attr with value "def_val" to all cc_binary rules
# Note that special characters will automatically be escaped in the string
buildozer 'add new_attr def_val' //:%cc_binary
+
+# Specifying an argument type to prevent automatic quoting
+buildozer 'add tags:expr FOO' //pkg:rule # adds tags = [FOO]
```
### Print commands
diff --git a/buildozer/buildozer_test.sh b/buildozer/buildozer_test.sh
index 955c582..8ed014b 100755
--- a/buildozer/buildozer_test.sh
+++ b/buildozer/buildozer_test.sh
@@ -224,6 +224,34 @@
)'
}
+function test_add_override_expr() {
+ in='go_library(
+ name = "edit",
+ deps = ["build"],
+)'
+ run "$in" 'add deps:expr build' '//pkg:edit'
+ assert_equals 'go_library(
+ name = "edit",
+ deps = [
+ build,
+ "build",
+ ],
+)'
+}
+
+function test_add_override_unknown_type() {
+ in='go_library(
+ name = "edit",
+ deps = ["build"],
+)'
+ ERROR=2 run "$in" 'add deps:foo build' '//pkg:edit'
+ assert_equals 'go_library(
+ name = "edit",
+ deps = ["build"],
+)'
+}
+
+
function test_remove_last_dep() {
run "$one_dep" 'remove deps //buildifier:build' '//pkg:edit'
assert_equals 'go_library(name = "edit")'
@@ -1085,6 +1113,16 @@
)'
}
+function test_set_if_absent_override_expr() {
+ in='soy_js(name = "a")'
+
+ run "$in" 'set_if_absent copts:expr foo()' '//pkg:a'
+ assert_equals 'soy_js(
+ name = "a",
+ copts = foo(),
+)'
+}
+
function test_set_custom_code() {
in='cc_test(name = "a")'
@@ -1098,6 +1136,16 @@
)'
}
+function test_set_override_expr() {
+ in='cc_library(name = "a")'
+
+ run "$in" 'set copts:expr foo()' '//pkg:a'
+ assert_equals 'cc_library(
+ name = "a",
+ copts = foo(),
+)'
+}
+
function assert_output() {
echo "$1" > "expected"
diff -u "expected" "$log" || fail "Output didn't match"
diff --git a/edit/buildozer.go b/edit/buildozer.go
index d246bfa..e896df4 100644
--- a/edit/buildozer.go
+++ b/edit/buildozer.go
@@ -83,18 +83,52 @@
output *apipb.Output_Record // output proto, stores whatever a command wants to print
}
+// AttrType is an enum representing a type of the attribute, e.g. expr or not
+// provided type
+type AttrType int
+
+const (
+ // notProvidedTypeAttr represents an attr type that's not provided explicitly and
+ // needs to be inferred by buildozer from bazel's build language proto
+ notProvidedTypeAttr = iota
+ // rawAttr is a type for idents, ints or other types that buildozer should
+ // take as is, without quoting or wrapping
+ rawAttr
+)
+
+// parseAttr parses the attr name and optional type, e.g. "foo" or "bar:expr"
+// The only supported type now is `expr`
+func parseAttr(attrName string) (attr string, attrType AttrType, err error) {
+ chunks := strings.SplitN(attrName, ":", 2)
+ if len(chunks) == 1 {
+ return attrName, notProvidedTypeAttr, nil
+ }
+ attr, typeName := chunks[0], chunks[1]
+ switch typeName {
+ case "expr":
+ return attr, rawAttr, nil
+ default:
+ return attr, notProvidedTypeAttr, fmt.Errorf("unknown attr type: %q", typeName)
+ }
+}
+
// The cmdXXX functions implement the various commands.
func cmdAdd(opts *Options, env CmdEnvironment) (*build.File, error) {
- attr := env.Args[0]
+ attr, attrType, err := parseAttr(env.Args[0])
+ if err != nil {
+ return nil, err
+ }
for _, val := range env.Args[1:] {
- if IsIntList(attr) {
+ if attrType == rawAttr || (attrType == notProvidedTypeAttr && IsIntList(attr)) {
AddValueToListAttribute(env.Rule, attr, env.Pkg, &build.LiteralExpr{Token: val}, &env.Vars)
continue
}
- strVal := getStringExpr(val, env.Pkg)
+ var strVal build.Expr
+ strVal = getLabelStringExpr(val, env.Pkg)
AddValueToListAttribute(env.Rule, attr, env.Pkg, strVal, &env.Vars)
}
+
ResolveAttr(env.Rule, attr, env.Pkg)
return env.File, nil
}
@@ -528,7 +562,7 @@
attr := env.Rule.Attr(key)
if e, ok := attr.(*build.StringExpr); ok {
if labels.Equal(e.Value, oldV, env.Pkg) {
- env.Rule.SetAttr(key, getAttrValueExpr(key, []string{newV}, env))
+ env.Rule.SetAttr(key, getAttrValueExpr(key, notProvidedTypeAttr, []string{newV}, env))
}
} else {
ListReplace(attr, oldV, newV, env.Pkg)
@@ -551,56 +585,71 @@
continue
}
if newValue, ok := stringSubstitute(e.Value, oldRegexp, newTemplate); ok {
- env.Rule.SetAttr(key, getStringExpr(newValue, env.Pkg))
+ env.Rule.SetAttr(key, getLabelStringExpr(newValue, env.Pkg))
}
}
return env.File, nil
}
func cmdSet(opts *Options, env CmdEnvironment) (*build.File, error) {
- attr := env.Args[0]
+ attr, attrType, err := parseAttr(env.Args[0])
+ if err != nil {
+ return nil, err
+ }
args := env.Args[1:]
if attr == "kind" {
env.Rule.SetKind(args[0])
} else {
- env.Rule.SetAttr(attr, getAttrValueExpr(attr, args, env))
+ env.Rule.SetAttr(attr, getAttrValueExpr(attr, attrType, args, env))
}
return env.File, nil
}
func cmdSetIfAbsent(opts *Options, env CmdEnvironment) (*build.File, error) {
- attr := env.Args[0]
+ attr, attrType, err := parseAttr(env.Args[0])
+ if err != nil {
+ return nil, err
+ }
args := env.Args[1:]
if attr == "kind" {
return nil, fmt.Errorf("setting 'kind' is not allowed for set_if_absent. Got %s", env.Args)
}
if env.Rule.Attr(attr) == nil {
- env.Rule.SetAttr(attr, getAttrValueExpr(attr, args, env))
+ env.Rule.SetAttr(attr, getAttrValueExpr(attr, attrType, args, env))
}
return env.File, nil
}
-func getAttrValueExpr(attr string, args []string, env CmdEnvironment) build.Expr {
+func getAttrValueExpr(attr string, attrType AttrType, args []string, env CmdEnvironment) build.Expr {
switch {
case attr == "kind":
return nil
+ case attrType == rawAttr:
+ // treat as an ident (it could be an arbitrary expression though, e.g. an
+ // already quoted string or a function call, will be re-parsed and
+ // formatted properly by the subsequent call of buildifier on the
+ //resulting file)
+ return &build.Ident{Name: args[0]}
case IsIntList(attr):
+ // list of raw objects (e.g. ints or idents)
var list []build.Expr
for _, i := range args {
list = append(list, &build.LiteralExpr{Token: i})
}
return &build.ListExpr{List: list}
case IsList(attr) && !(len(args) == 1 && strings.HasPrefix(args[0], "glob(")):
+ // list of labels
var list []build.Expr
for _, arg := range args {
- list = append(list, getStringExpr(arg, env.Pkg))
+ list = append(list, getLabelStringExpr(arg, env.Pkg))
}
return &build.ListExpr{List: list}
case len(args) == 0:
// Expected a non-list argument, nothing provided
return &build.Ident{Name: "None"}
case IsString(attr):
- return getStringExpr(args[0], env.Pkg)
+ // single label
+ return getLabelStringExpr(args[0], env.Pkg)
default:
return &build.Ident{Name: args[0]}
}
@@ -614,15 +663,24 @@
return value
}
-// getStringExpr creates a StringExpr from an input argument, which can be either quoted or not,
+// getStringExprLabel creates a StringExpr from an input argument, which can be either quoted or not,
// and shortens the label value if possible.
-func getStringExpr(value, pkg string) build.Expr {
+func getLabelStringExpr(value, pkg string) build.Expr {
if unquoted, triple, err := build.Unquote(value); err == nil {
return &build.StringExpr{Value: ShortenLabel(unquoted, pkg), TripleQuote: triple}
}
return &build.StringExpr{Value: ShortenLabel(value, pkg)}
}
+// getListExpr creates a ListExpr from a list of input arguments
+func getListExpr(exprs ...string) *build.ListExpr {
+ listVal := &build.ListExpr{}
+ for _, expr := range exprs {
+ listVal.List = append(listVal.List, &build.Ident{Name: expr})
+ }
+ return listVal
+}
+
func cmdCopy(opts *Options, env CmdEnvironment) (*build.File, error) {
attrName := env.Args[0]
from := env.Args[1]
@@ -658,7 +716,7 @@
return nil, fmt.Errorf("no colon in dict_add argument %q found", x)
}
kv = removeEscapes(kv, ':')
- expr := getStringExpr(kv[1], env.Pkg)
+ expr := getLabelStringExpr(kv[1], env.Pkg)
prev := DictionaryGet(dict, kv[0])
if prev == nil {
@@ -688,10 +746,10 @@
if cur := DictionaryGet(dict, key); cur != nil {
list = cur.(*build.ListExpr)
}
- AddValueToList(list, env.Pkg, getStringExpr(value, env.Pkg), !attributeMustNotBeSorted(env.Rule.Name(), attr))
+ AddValueToList(list, env.Pkg, getLabelStringExpr(value, env.Pkg), !attributeMustNotBeSorted(env.Rule.Name(), attr))
expr = list
} else {
- expr = getStringExpr(value, env.Pkg)
+ expr = getLabelStringExpr(value, env.Pkg)
}
// Set overwrites previous values.
DictionarySet(dict, key, expr)
@@ -719,7 +777,7 @@
return nil, fmt.Errorf("no colon in dict_set argument %q found", x)
}
kv = removeEscapes(kv, ':')
- expr := getStringExpr(kv[1], env.Pkg)
+ expr := getLabelStringExpr(kv[1], env.Pkg)
// Set overwrites previous values.
DictionarySet(dict, kv[0], expr)
}
@@ -777,11 +835,11 @@
// Key-Value Pair matches key.
if val, ok := kv.Value.(*build.StringExpr); ok {
if labels.Equal(val.Value, oldV, env.Pkg) {
- kv.Value = getStringExpr(newV, env.Pkg)
+ kv.Value = getLabelStringExpr(newV, env.Pkg)
}
} else if val, ok := kv.Value.(*build.Ident); ok {
if val.Name == oldV {
- kv.Value = getStringExpr(newV, env.Pkg)
+ kv.Value = getLabelStringExpr(newV, env.Pkg)
}
}
}
@@ -807,7 +865,7 @@
}
for _, val := range args {
- expr := getStringExpr(val, env.Pkg)
+ expr := getLabelStringExpr(val, env.Pkg)
prev = AddValueToList(prev, env.Pkg, expr, true)
}