Add package for printing starlark formatted data
Bug: 216168792
Test: build/bazel/ci/bp2build.sh
Change-Id: I3a06b19396f7ffe1c638042cda7e731dd840f1d6
diff --git a/android/Android.bp b/android/Android.bp
index da36959..d3540b2 100644
--- a/android/Android.bp
+++ b/android/Android.bp
@@ -16,7 +16,9 @@
"soong-remoteexec",
"soong-response",
"soong-shared",
+ "soong-starlark-format",
"soong-ui-metrics_proto",
+
"golang-protobuf-proto",
"golang-protobuf-encoding-prototext",
diff --git a/android/config.go b/android/config.go
index 10e074c..f10732b 100644
--- a/android/config.go
+++ b/android/config.go
@@ -38,6 +38,7 @@
"android/soong/android/soongconfig"
"android/soong/bazel"
"android/soong/remoteexec"
+ "android/soong/starlark_fmt"
)
// Bool re-exports proptools.Bool for the android package.
@@ -286,14 +287,12 @@
}
}
- //TODO(b/216168792) should use common function to print Starlark code
- nonArchVariantProductVariablesJson, err := json.MarshalIndent(&nonArchVariantProductVariables, "", " ")
+ nonArchVariantProductVariablesJson := starlark_fmt.PrintStringList(nonArchVariantProductVariables, 0)
if err != nil {
return fmt.Errorf("cannot marshal product variable data: %s", err.Error())
}
- //TODO(b/216168792) should use common function to print Starlark code
- archVariantProductVariablesJson, err := json.MarshalIndent(&archVariantProductVariables, "", " ")
+ archVariantProductVariablesJson := starlark_fmt.PrintStringList(archVariantProductVariables, 0)
if err != nil {
return fmt.Errorf("cannot marshal arch variant product variable data: %s", err.Error())
}
diff --git a/android/soong_config_modules_test.go b/android/soong_config_modules_test.go
index acb9d18..ceb8e45 100644
--- a/android/soong_config_modules_test.go
+++ b/android/soong_config_modules_test.go
@@ -386,6 +386,46 @@
})).RunTest(t)
}
+func TestDuplicateStringValueInSoongConfigStringVariable(t *testing.T) {
+ bp := `
+ soong_config_string_variable {
+ name: "board",
+ values: ["soc_a", "soc_b", "soc_c", "soc_a"],
+ }
+
+ soong_config_module_type {
+ name: "acme_test",
+ module_type: "test",
+ config_namespace: "acme",
+ variables: ["board"],
+ properties: ["cflags", "srcs", "defaults"],
+ }
+ `
+
+ fixtureForVendorVars := func(vars map[string]map[string]string) FixturePreparer {
+ return FixtureModifyProductVariables(func(variables FixtureProductVariables) {
+ variables.VendorVars = vars
+ })
+ }
+
+ GroupFixturePreparers(
+ fixtureForVendorVars(map[string]map[string]string{"acme": {"feature1": "1"}}),
+ PrepareForTestWithDefaults,
+ FixtureRegisterWithContext(func(ctx RegistrationContext) {
+ ctx.RegisterModuleType("soong_config_module_type_import", SoongConfigModuleTypeImportFactory)
+ ctx.RegisterModuleType("soong_config_module_type", SoongConfigModuleTypeFactory)
+ ctx.RegisterModuleType("soong_config_string_variable", SoongConfigStringVariableDummyFactory)
+ ctx.RegisterModuleType("soong_config_bool_variable", SoongConfigBoolVariableDummyFactory)
+ ctx.RegisterModuleType("test_defaults", soongConfigTestDefaultsModuleFactory)
+ ctx.RegisterModuleType("test", soongConfigTestModuleFactory)
+ }),
+ FixtureWithRootAndroidBp(bp),
+ ).ExtendWithErrorHandler(FixtureExpectsAllErrorsToMatchAPattern([]string{
+ // TODO(b/171232169): improve the error message for non-existent properties
+ `Android.bp: soong_config_string_variable: values property error: duplicate value: "soc_a"`,
+ })).RunTest(t)
+}
+
func testConfigWithVendorVars(buildDir, bp string, fs map[string][]byte, vendorVars map[string]map[string]string) Config {
config := TestConfig(buildDir, nil, bp, fs)
diff --git a/android/soongconfig/Android.bp b/android/soongconfig/Android.bp
index 9bf3344..8fe1ff1 100644
--- a/android/soongconfig/Android.bp
+++ b/android/soongconfig/Android.bp
@@ -10,6 +10,7 @@
"blueprint-parser",
"blueprint-proptools",
"soong-bazel",
+ "soong-starlark-format",
],
srcs: [
"config.go",
diff --git a/android/soongconfig/modules.go b/android/soongconfig/modules.go
index 09a5057..212b752 100644
--- a/android/soongconfig/modules.go
+++ b/android/soongconfig/modules.go
@@ -25,6 +25,8 @@
"github.com/google/blueprint"
"github.com/google/blueprint/parser"
"github.com/google/blueprint/proptools"
+
+ "android/soong/starlark_fmt"
)
const conditionsDefault = "conditions_default"
@@ -177,10 +179,14 @@
return []error{fmt.Errorf("values property must be set")}
}
+ vals := make(map[string]bool, len(stringProps.Values))
for _, name := range stringProps.Values {
if err := checkVariableName(name); err != nil {
return []error{fmt.Errorf("soong_config_string_variable: values property error %s", err)}
+ } else if _, ok := vals[name]; ok {
+ return []error{fmt.Errorf("soong_config_string_variable: values property error: duplicate value: %q", name)}
}
+ vals[name] = true
}
v.variables[base.variable] = &stringVariable{
@@ -235,7 +241,12 @@
// string vars, bool vars and value vars created by every
// soong_config_module_type in this build.
type Bp2BuildSoongConfigDefinitions struct {
- StringVars map[string]map[string]bool
+ // varCache contains a cache of string variables namespace + property
+ // The same variable may be used in multiple module types (for example, if need support
+ // for cc_default and java_default), only need to process once
+ varCache map[string]bool
+
+ StringVars map[string][]string
BoolVars map[string]bool
ValueVars map[string]bool
}
@@ -253,7 +264,7 @@
defer bp2buildSoongConfigVarsLock.Unlock()
if defs.StringVars == nil {
- defs.StringVars = make(map[string]map[string]bool)
+ defs.StringVars = make(map[string][]string)
}
if defs.BoolVars == nil {
defs.BoolVars = make(map[string]bool)
@@ -261,15 +272,24 @@
if defs.ValueVars == nil {
defs.ValueVars = make(map[string]bool)
}
+ if defs.varCache == nil {
+ defs.varCache = make(map[string]bool)
+ }
for _, moduleType := range mtDef.ModuleTypes {
for _, v := range moduleType.Variables {
key := strings.Join([]string{moduleType.ConfigNamespace, v.variableProperty()}, "__")
+
+ // The same variable may be used in multiple module types (for example, if need support
+ // for cc_default and java_default), only need to process once
+ if _, keyInCache := defs.varCache[key]; keyInCache {
+ continue
+ } else {
+ defs.varCache[key] = true
+ }
+
if strVar, ok := v.(*stringVariable); ok {
- if _, ok := defs.StringVars[key]; !ok {
- defs.StringVars[key] = make(map[string]bool, 0)
- }
for _, value := range strVar.values {
- defs.StringVars[key][value] = true
+ defs.StringVars[key] = append(defs.StringVars[key], value)
}
} else if _, ok := v.(*boolVariable); ok {
defs.BoolVars[key] = true
@@ -302,29 +322,16 @@
// String emits the Soong config variable definitions as Starlark dictionaries.
func (defs Bp2BuildSoongConfigDefinitions) String() string {
ret := ""
- ret += "soong_config_bool_variables = {\n"
- for _, boolVar := range sortedStringKeys(defs.BoolVars) {
- ret += fmt.Sprintf(" \"%s\": True,\n", boolVar)
- }
- ret += "}\n"
- ret += "\n"
+ ret += "soong_config_bool_variables = "
+ ret += starlark_fmt.PrintBoolDict(defs.BoolVars, 0)
+ ret += "\n\n"
- ret += "soong_config_value_variables = {\n"
- for _, valueVar := range sortedStringKeys(defs.ValueVars) {
- ret += fmt.Sprintf(" \"%s\": True,\n", valueVar)
- }
- ret += "}\n"
- ret += "\n"
+ ret += "soong_config_value_variables = "
+ ret += starlark_fmt.PrintBoolDict(defs.ValueVars, 0)
+ ret += "\n\n"
- ret += "soong_config_string_variables = {\n"
- for _, stringVar := range sortedStringKeys(defs.StringVars) {
- ret += fmt.Sprintf(" \"%s\": [\n", stringVar)
- for _, choice := range sortedStringKeys(defs.StringVars[stringVar]) {
- ret += fmt.Sprintf(" \"%s\",\n", choice)
- }
- ret += fmt.Sprintf(" ],\n")
- }
- ret += "}"
+ ret += "soong_config_string_variables = "
+ ret += starlark_fmt.PrintStringListDict(defs.StringVars, 0)
return ret
}
diff --git a/android/soongconfig/modules_test.go b/android/soongconfig/modules_test.go
index b14f8b4..a7800e8 100644
--- a/android/soongconfig/modules_test.go
+++ b/android/soongconfig/modules_test.go
@@ -367,19 +367,19 @@
func Test_Bp2BuildSoongConfigDefinitions(t *testing.T) {
testCases := []struct {
+ desc string
defs Bp2BuildSoongConfigDefinitions
expected string
}{
{
+ desc: "all empty",
defs: Bp2BuildSoongConfigDefinitions{},
- expected: `soong_config_bool_variables = {
-}
+ expected: `soong_config_bool_variables = {}
-soong_config_value_variables = {
-}
+soong_config_value_variables = {}
-soong_config_string_variables = {
-}`}, {
+soong_config_string_variables = {}`}, {
+ desc: "only bool",
defs: Bp2BuildSoongConfigDefinitions{
BoolVars: map[string]bool{
"bool_var": true,
@@ -389,39 +389,35 @@
"bool_var": True,
}
-soong_config_value_variables = {
-}
+soong_config_value_variables = {}
-soong_config_string_variables = {
-}`}, {
+soong_config_string_variables = {}`}, {
+ desc: "only value vars",
defs: Bp2BuildSoongConfigDefinitions{
ValueVars: map[string]bool{
"value_var": true,
},
},
- expected: `soong_config_bool_variables = {
-}
+ expected: `soong_config_bool_variables = {}
soong_config_value_variables = {
"value_var": True,
}
-soong_config_string_variables = {
-}`}, {
+soong_config_string_variables = {}`}, {
+ desc: "only string vars",
defs: Bp2BuildSoongConfigDefinitions{
- StringVars: map[string]map[string]bool{
- "string_var": map[string]bool{
- "choice1": true,
- "choice2": true,
- "choice3": true,
+ StringVars: map[string][]string{
+ "string_var": []string{
+ "choice1",
+ "choice2",
+ "choice3",
},
},
},
- expected: `soong_config_bool_variables = {
-}
+ expected: `soong_config_bool_variables = {}
-soong_config_value_variables = {
-}
+soong_config_value_variables = {}
soong_config_string_variables = {
"string_var": [
@@ -430,6 +426,7 @@
"choice3",
],
}`}, {
+ desc: "all vars",
defs: Bp2BuildSoongConfigDefinitions{
BoolVars: map[string]bool{
"bool_var_one": true,
@@ -438,15 +435,15 @@
"value_var_one": true,
"value_var_two": true,
},
- StringVars: map[string]map[string]bool{
- "string_var_one": map[string]bool{
- "choice1": true,
- "choice2": true,
- "choice3": true,
+ StringVars: map[string][]string{
+ "string_var_one": []string{
+ "choice1",
+ "choice2",
+ "choice3",
},
- "string_var_two": map[string]bool{
- "foo": true,
- "bar": true,
+ "string_var_two": []string{
+ "foo",
+ "bar",
},
},
},
@@ -466,15 +463,17 @@
"choice3",
],
"string_var_two": [
- "bar",
"foo",
+ "bar",
],
}`},
}
for _, test := range testCases {
- actual := test.defs.String()
- if actual != test.expected {
- t.Errorf("Expected:\n%s\nbut got:\n%s", test.expected, actual)
- }
+ t.Run(test.desc, func(t *testing.T) {
+ actual := test.defs.String()
+ if actual != test.expected {
+ t.Errorf("Expected:\n%s\nbut got:\n%s", test.expected, actual)
+ }
+ })
}
}
diff --git a/bp2build/Android.bp b/bp2build/Android.bp
index 4bcfa61..b904c35 100644
--- a/bp2build/Android.bp
+++ b/bp2build/Android.bp
@@ -28,6 +28,7 @@
"soong-genrule",
"soong-python",
"soong-sh",
+ "soong-starlark-format",
"soong-ui-metrics",
],
testSrcs: [
diff --git a/bp2build/build_conversion.go b/bp2build/build_conversion.go
index b3bec65..1d3b105 100644
--- a/bp2build/build_conversion.go
+++ b/bp2build/build_conversion.go
@@ -27,6 +27,7 @@
"android/soong/android"
"android/soong/bazel"
+ "android/soong/starlark_fmt"
"github.com/google/blueprint"
"github.com/google/blueprint/proptools"
@@ -559,48 +560,27 @@
return "", nil
}
- var ret string
switch propertyValue.Kind() {
case reflect.String:
- ret = fmt.Sprintf("\"%v\"", escapeString(propertyValue.String()))
+ return fmt.Sprintf("\"%v\"", escapeString(propertyValue.String())), nil
case reflect.Bool:
- ret = strings.Title(fmt.Sprintf("%v", propertyValue.Interface()))
+ return starlark_fmt.PrintBool(propertyValue.Bool()), nil
case reflect.Int, reflect.Uint, reflect.Int64:
- ret = fmt.Sprintf("%v", propertyValue.Interface())
+ return fmt.Sprintf("%v", propertyValue.Interface()), nil
case reflect.Ptr:
return prettyPrint(propertyValue.Elem(), indent, emitZeroValues)
case reflect.Slice:
- if propertyValue.Len() == 0 {
- return "[]", nil
- }
-
- if propertyValue.Len() == 1 {
- // Single-line list for list with only 1 element
- ret += "["
- indexedValue, err := prettyPrint(propertyValue.Index(0), indent, emitZeroValues)
+ elements := make([]string, 0, propertyValue.Len())
+ for i := 0; i < propertyValue.Len(); i++ {
+ val, err := prettyPrint(propertyValue.Index(i), indent, emitZeroValues)
if err != nil {
return "", err
}
- ret += indexedValue
- ret += "]"
- } else {
- // otherwise, use a multiline list.
- ret += "[\n"
- for i := 0; i < propertyValue.Len(); i++ {
- indexedValue, err := prettyPrint(propertyValue.Index(i), indent+1, emitZeroValues)
- if err != nil {
- return "", err
- }
-
- if indexedValue != "" {
- ret += makeIndent(indent + 1)
- ret += indexedValue
- ret += ",\n"
- }
+ if val != "" {
+ elements = append(elements, val)
}
- ret += makeIndent(indent)
- ret += "]"
}
+ return starlark_fmt.PrintList(elements, indent, "%s"), nil
case reflect.Struct:
// Special cases where the bp2build sends additional information to the codegenerator
@@ -611,18 +591,12 @@
return fmt.Sprintf("%q", label.Label), nil
}
- ret = "{\n"
// Sort and print the struct props by the key.
structProps := extractStructProperties(propertyValue, indent)
if len(structProps) == 0 {
return "", nil
}
- for _, k := range android.SortedStringKeys(structProps) {
- ret += makeIndent(indent + 1)
- ret += fmt.Sprintf("%q: %s,\n", k, structProps[k])
- }
- ret += makeIndent(indent)
- ret += "}"
+ return starlark_fmt.PrintDict(structProps, indent), nil
case reflect.Interface:
// TODO(b/164227191): implement pretty print for interfaces.
// Interfaces are used for for arch, multilib and target properties.
@@ -631,7 +605,6 @@
return "", fmt.Errorf(
"unexpected kind for property struct field: %s", propertyValue.Kind())
}
- return ret, nil
}
// Converts a reflected property struct value into a map of property names and property values,
@@ -736,13 +709,6 @@
return strings.ReplaceAll(s, "\"", "\\\"")
}
-func makeIndent(indent int) string {
- if indent < 0 {
- panic(fmt.Errorf("indent column cannot be less than 0, but got %d", indent))
- }
- return strings.Repeat(" ", indent)
-}
-
func targetNameWithVariant(c bpToBuildContext, logicModule blueprint.Module) string {
name := ""
if c.ModuleSubDir(logicModule) != "" {
diff --git a/bp2build/configurability.go b/bp2build/configurability.go
index dfbb265..d37a523 100644
--- a/bp2build/configurability.go
+++ b/bp2build/configurability.go
@@ -6,6 +6,7 @@
"android/soong/android"
"android/soong/bazel"
+ "android/soong/starlark_fmt"
)
// Configurability support for bp2build.
@@ -250,10 +251,10 @@
} else if defaultValue != nil {
// Print an explicit empty list (the default value) even if the value is
// empty, to avoid errors about not finding a configuration that matches.
- ret += fmt.Sprintf("%s\"%s\": %s,\n", makeIndent(indent+1), bazel.ConditionsDefaultSelectKey, *defaultValue)
+ ret += fmt.Sprintf("%s\"%s\": %s,\n", starlark_fmt.Indention(indent+1), bazel.ConditionsDefaultSelectKey, *defaultValue)
}
- ret += makeIndent(indent)
+ ret += starlark_fmt.Indention(indent)
ret += "})"
return ret, nil
@@ -262,7 +263,7 @@
// prettyPrintSelectEntry converts a reflect.Value into an entry in a select map
// with a provided key.
func prettyPrintSelectEntry(value reflect.Value, key string, indent int, emitZeroValues bool) (string, error) {
- s := makeIndent(indent + 1)
+ s := starlark_fmt.Indention(indent + 1)
v, err := prettyPrint(value, indent+1, emitZeroValues)
if err != nil {
return "", err
diff --git a/cc/config/Android.bp b/cc/config/Android.bp
index 7b7ee28..e1b0605 100644
--- a/cc/config/Android.bp
+++ b/cc/config/Android.bp
@@ -8,6 +8,7 @@
deps: [
"soong-android",
"soong-remoteexec",
+ "soong-starlark-format",
],
srcs: [
"bp2build.go",
diff --git a/cc/config/bp2build.go b/cc/config/bp2build.go
index 982b436..eca5161 100644
--- a/cc/config/bp2build.go
+++ b/cc/config/bp2build.go
@@ -22,14 +22,11 @@
"strings"
"android/soong/android"
+ "android/soong/starlark_fmt"
"github.com/google/blueprint"
)
-const (
- bazelIndent = 4
-)
-
type bazelVarExporter interface {
asBazel(android.Config, exportedStringVariables, exportedStringListVariables, exportedConfigDependingVariables) []bazelConstant
}
@@ -73,21 +70,6 @@
m[k] = v
}
-func bazelIndention(level int) string {
- return strings.Repeat(" ", level*bazelIndent)
-}
-
-func printBazelList(items []string, indentLevel int) string {
- list := make([]string, 0, len(items)+2)
- list = append(list, "[")
- innerIndent := bazelIndention(indentLevel + 1)
- for _, item := range items {
- list = append(list, fmt.Sprintf(`%s"%s",`, innerIndent, item))
- }
- list = append(list, bazelIndention(indentLevel)+"]")
- return strings.Join(list, "\n")
-}
-
func (m exportedStringVariables) asBazel(config android.Config,
stringVars exportedStringVariables, stringListVars exportedStringListVariables, cfgDepVars exportedConfigDependingVariables) []bazelConstant {
ret := make([]bazelConstant, 0, len(m))
@@ -139,7 +121,7 @@
// out through a constants struct later.
ret = append(ret, bazelConstant{
variableName: k,
- internalDefinition: printBazelList(expandedVars, 0),
+ internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0),
})
}
return ret
@@ -173,17 +155,6 @@
m[k] = v
}
-func printBazelStringListDict(dict map[string][]string) string {
- bazelDict := make([]string, 0, len(dict)+2)
- bazelDict = append(bazelDict, "{")
- for k, v := range dict {
- bazelDict = append(bazelDict,
- fmt.Sprintf(`%s"%s": %s,`, bazelIndention(1), k, printBazelList(v, 1)))
- }
- bazelDict = append(bazelDict, "}")
- return strings.Join(bazelDict, "\n")
-}
-
// Since dictionaries are not supported in Ninja, we do not expand variables for dictionaries
func (m exportedStringListDictVariables) asBazel(_ android.Config, _ exportedStringVariables,
_ exportedStringListVariables, _ exportedConfigDependingVariables) []bazelConstant {
@@ -191,7 +162,7 @@
for k, dict := range m {
ret = append(ret, bazelConstant{
variableName: k,
- internalDefinition: printBazelStringListDict(dict),
+ internalDefinition: starlark_fmt.PrintStringListDict(dict, 0),
})
}
return ret
@@ -223,7 +194,7 @@
definitions = append(definitions,
fmt.Sprintf("_%s = %s", b.variableName, b.internalDefinition))
constants = append(constants,
- fmt.Sprintf("%[1]s%[2]s = _%[2]s,", bazelIndention(1), b.variableName))
+ fmt.Sprintf("%[1]s%[2]s = _%[2]s,", starlark_fmt.Indention(1), b.variableName))
}
// Build the exported constants struct.
diff --git a/cc/config/bp2build_test.go b/cc/config/bp2build_test.go
index 3118df1..4cbf0c6 100644
--- a/cc/config/bp2build_test.go
+++ b/cc/config/bp2build_test.go
@@ -211,15 +211,11 @@
expectedOut: `# GENERATED FOR BAZEL FROM SOONG. DO NOT EDIT.
_a = {
- "b1": [
- "b2",
- ],
+ "b1": ["b2"],
}
_c = {
- "d1": [
- "d2",
- ],
+ "d1": ["d2"],
}
constants = struct(
@@ -246,27 +242,19 @@
expectedOut: `# GENERATED FOR BAZEL FROM SOONG. DO NOT EDIT.
_a = {
- "a1": [
- "a2",
- ],
+ "a1": ["a2"],
}
_b = "b-val"
-_c = [
- "c-val",
-]
+_c = ["c-val"]
_d = "d-val"
-_e = [
- "e-val",
-]
+_e = ["e-val"]
_f = {
- "f1": [
- "f2",
- ],
+ "f1": ["f2"],
}
constants = struct(
diff --git a/starlark_fmt/Android.bp b/starlark_fmt/Android.bp
new file mode 100644
index 0000000..8d80ccd
--- /dev/null
+++ b/starlark_fmt/Android.bp
@@ -0,0 +1,28 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+ name: "soong-starlark-format",
+ pkgPath: "android/soong/starlark_fmt",
+ srcs: [
+ "format.go",
+ ],
+ testSrcs: [
+ "format_test.go",
+ ],
+}
diff --git a/starlark_fmt/format.go b/starlark_fmt/format.go
new file mode 100644
index 0000000..23eee59
--- /dev/null
+++ b/starlark_fmt/format.go
@@ -0,0 +1,96 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package starlark_fmt
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+const (
+ indent = 4
+)
+
+// Indention returns an indent string of the specified level.
+func Indention(level int) string {
+ if level < 0 {
+ panic(fmt.Errorf("indent level cannot be less than 0, but got %d", level))
+ }
+ return strings.Repeat(" ", level*indent)
+}
+
+// PrintBool returns a Starlark compatible bool string.
+func PrintBool(item bool) string {
+ return strings.Title(fmt.Sprintf("%t", item))
+}
+
+// PrintsStringList returns a Starlark-compatible string of a list of Strings/Labels.
+func PrintStringList(items []string, indentLevel int) string {
+ return PrintList(items, indentLevel, `"%s"`)
+}
+
+// PrintList returns a Starlark-compatible string of list formmated as requested.
+func PrintList(items []string, indentLevel int, formatString string) string {
+ if len(items) == 0 {
+ return "[]"
+ } else if len(items) == 1 {
+ return fmt.Sprintf("["+formatString+"]", items[0])
+ }
+ list := make([]string, 0, len(items)+2)
+ list = append(list, "[")
+ innerIndent := Indention(indentLevel + 1)
+ for _, item := range items {
+ list = append(list, fmt.Sprintf(`%s`+formatString+`,`, innerIndent, item))
+ }
+ list = append(list, Indention(indentLevel)+"]")
+ return strings.Join(list, "\n")
+}
+
+// PrintStringListDict returns a Starlark-compatible string formatted as dictionary with
+// string keys and list of string values.
+func PrintStringListDict(dict map[string][]string, indentLevel int) string {
+ formattedValueDict := make(map[string]string, len(dict))
+ for k, v := range dict {
+ formattedValueDict[k] = PrintStringList(v, indentLevel+1)
+ }
+ return PrintDict(formattedValueDict, indentLevel)
+}
+
+// PrintBoolDict returns a starlark-compatible string containing a dictionary with string keys and
+// values printed with no additional formatting.
+func PrintBoolDict(dict map[string]bool, indentLevel int) string {
+ formattedValueDict := make(map[string]string, len(dict))
+ for k, v := range dict {
+ formattedValueDict[k] = PrintBool(v)
+ }
+ return PrintDict(formattedValueDict, indentLevel)
+}
+
+// PrintDict returns a starlark-compatible string containing a dictionary with string keys and
+// values printed with no additional formatting.
+func PrintDict(dict map[string]string, indentLevel int) string {
+ if len(dict) == 0 {
+ return "{}"
+ }
+ items := make([]string, 0, len(dict))
+ for k, v := range dict {
+ items = append(items, fmt.Sprintf(`%s"%s": %s,`, Indention(indentLevel+1), k, v))
+ }
+ sort.Strings(items)
+ return fmt.Sprintf(`{
+%s
+%s}`, strings.Join(items, "\n"), Indention(indentLevel))
+}
diff --git a/starlark_fmt/format_test.go b/starlark_fmt/format_test.go
new file mode 100644
index 0000000..90f78ef
--- /dev/null
+++ b/starlark_fmt/format_test.go
@@ -0,0 +1,169 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package starlark_fmt
+
+import (
+ "testing"
+)
+
+func TestPrintEmptyStringList(t *testing.T) {
+ in := []string{}
+ indentLevel := 0
+ out := PrintStringList(in, indentLevel)
+ expectedOut := "[]"
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintSingleElementStringList(t *testing.T) {
+ in := []string{"a"}
+ indentLevel := 0
+ out := PrintStringList(in, indentLevel)
+ expectedOut := `["a"]`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintMultiElementStringList(t *testing.T) {
+ in := []string{"a", "b"}
+ indentLevel := 0
+ out := PrintStringList(in, indentLevel)
+ expectedOut := `[
+ "a",
+ "b",
+]`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintEmptyList(t *testing.T) {
+ in := []string{}
+ indentLevel := 0
+ out := PrintList(in, indentLevel, "%s")
+ expectedOut := "[]"
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintSingleElementList(t *testing.T) {
+ in := []string{"1"}
+ indentLevel := 0
+ out := PrintList(in, indentLevel, "%s")
+ expectedOut := `[1]`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintMultiElementList(t *testing.T) {
+ in := []string{"1", "2"}
+ indentLevel := 0
+ out := PrintList(in, indentLevel, "%s")
+ expectedOut := `[
+ 1,
+ 2,
+]`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestListWithNonZeroIndent(t *testing.T) {
+ in := []string{"1", "2"}
+ indentLevel := 1
+ out := PrintList(in, indentLevel, "%s")
+ expectedOut := `[
+ 1,
+ 2,
+ ]`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestStringListDictEmpty(t *testing.T) {
+ in := map[string][]string{}
+ indentLevel := 0
+ out := PrintStringListDict(in, indentLevel)
+ expectedOut := `{}`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestStringListDict(t *testing.T) {
+ in := map[string][]string{
+ "key1": []string{},
+ "key2": []string{"a"},
+ "key3": []string{"1", "2"},
+ }
+ indentLevel := 0
+ out := PrintStringListDict(in, indentLevel)
+ expectedOut := `{
+ "key1": [],
+ "key2": ["a"],
+ "key3": [
+ "1",
+ "2",
+ ],
+}`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintDict(t *testing.T) {
+ in := map[string]string{
+ "key1": `""`,
+ "key2": `"a"`,
+ "key3": `[
+ 1,
+ 2,
+ ]`,
+ }
+ indentLevel := 0
+ out := PrintDict(in, indentLevel)
+ expectedOut := `{
+ "key1": "",
+ "key2": "a",
+ "key3": [
+ 1,
+ 2,
+ ],
+}`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}
+
+func TestPrintDictWithIndent(t *testing.T) {
+ in := map[string]string{
+ "key1": `""`,
+ "key2": `"a"`,
+ }
+ indentLevel := 1
+ out := PrintDict(in, indentLevel)
+ expectedOut := `{
+ "key1": "",
+ "key2": "a",
+ }`
+ if out != expectedOut {
+ t.Errorf("Expected %q, got %q", expectedOut, out)
+ }
+}