From 72beb34609217f3c98623fb6795b6417c7f0fc65 Mon Sep 17 00:00:00 2001 From: Liz Kammer Date: Thu, 3 Feb 2022 08:42:10 -0500 Subject: [PATCH] Add package for printing starlark formatted data Bug: 216168792 Test: build/bazel/ci/bp2build.sh Change-Id: I3a06b19396f7ffe1c638042cda7e731dd840f1d6 --- android/Android.bp | 2 + android/config.go | 7 +- android/soong_config_modules_test.go | 40 +++++++ android/soongconfig/Android.bp | 1 + android/soongconfig/modules.go | 61 +++++----- android/soongconfig/modules_test.go | 71 ++++++----- bp2build/Android.bp | 1 + bp2build/build_conversion.go | 56 ++------- bp2build/configurability.go | 7 +- cc/config/Android.bp | 1 + cc/config/bp2build.go | 37 +----- cc/config/bp2build_test.go | 24 +--- starlark_fmt/Android.bp | 28 +++++ starlark_fmt/format.go | 96 +++++++++++++++ starlark_fmt/format_test.go | 169 +++++++++++++++++++++++++++ 15 files changed, 435 insertions(+), 166 deletions(-) create mode 100644 starlark_fmt/Android.bp create mode 100644 starlark_fmt/format.go create mode 100644 starlark_fmt/format_test.go diff --git a/android/Android.bp b/android/Android.bp index da369592a..d3540b211 100644 --- a/android/Android.bp +++ b/android/Android.bp @@ -16,7 +16,9 @@ bootstrap_go_package { "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 10e074cb1..f10732bd7 100644 --- a/android/config.go +++ b/android/config.go @@ -38,6 +38,7 @@ import ( "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 @@ func saveToBazelConfigFile(config *productVariables, outDir string) error { } } - //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 acb9d180e..ceb8e45a5 100644 --- a/android/soong_config_modules_test.go +++ b/android/soong_config_modules_test.go @@ -386,6 +386,46 @@ func TestNonExistentPropertyInSoongConfigModule(t *testing.T) { })).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 9bf334480..8fe1ff1eb 100644 --- a/android/soongconfig/Android.bp +++ b/android/soongconfig/Android.bp @@ -10,6 +10,7 @@ bootstrap_go_package { "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 09a505722..212b752d6 100644 --- a/android/soongconfig/modules.go +++ b/android/soongconfig/modules.go @@ -25,6 +25,8 @@ import ( "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 @@ func processStringVariableDef(v *SoongConfigDefinition, def *parser.Module) (err 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 @@ type SoongConfigDefinition struct { // 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 @@ func (defs *Bp2BuildSoongConfigDefinitions) AddVars(mtDef SoongConfigDefinition) 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 @@ func (defs *Bp2BuildSoongConfigDefinitions) AddVars(mtDef SoongConfigDefinition) 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 @@ func sortedStringKeys(m interface{}) []string { // 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 b14f8b43b..a7800e8ef 100644 --- a/android/soongconfig/modules_test.go +++ b/android/soongconfig/modules_test.go @@ -367,19 +367,19 @@ func Test_PropertiesToApply(t *testing.T) { 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 @@ soong_config_string_variables = { "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 @@ soong_config_string_variables = { "choice3", ], }`}, { + desc: "all vars", defs: Bp2BuildSoongConfigDefinitions{ BoolVars: map[string]bool{ "bool_var_one": true, @@ -438,15 +435,15 @@ soong_config_string_variables = { "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 @@ soong_config_string_variables = { "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 4bcfa6110..b904c3533 100644 --- a/bp2build/Android.bp +++ b/bp2build/Android.bp @@ -28,6 +28,7 @@ bootstrap_go_package { "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 b3bec65da..1d3b10550 100644 --- a/bp2build/build_conversion.go +++ b/bp2build/build_conversion.go @@ -27,6 +27,7 @@ import ( "android/soong/android" "android/soong/bazel" + "android/soong/starlark_fmt" "github.com/google/blueprint" "github.com/google/blueprint/proptools" @@ -559,48 +560,27 @@ func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) ( 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 @@ func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) ( 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 @@ func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) ( 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 @@ func escapeString(s string) string { 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 dfbb265d2..d37a52394 100644 --- a/bp2build/configurability.go +++ b/bp2build/configurability.go @@ -6,6 +6,7 @@ import ( "android/soong/android" "android/soong/bazel" + "android/soong/starlark_fmt" ) // Configurability support for bp2build. @@ -250,10 +251,10 @@ func prettyPrintSelectMap(selectMap map[string]reflect.Value, defaultValue *stri } 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 @@ func prettyPrintSelectMap(selectMap map[string]reflect.Value, defaultValue *stri // 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 7b7ee2849..e1b06057b 100644 --- a/cc/config/Android.bp +++ b/cc/config/Android.bp @@ -8,6 +8,7 @@ bootstrap_go_package { deps: [ "soong-android", "soong-remoteexec", + "soong-starlark-format", ], srcs: [ "bp2build.go", diff --git a/cc/config/bp2build.go b/cc/config/bp2build.go index 982b43648..eca516107 100644 --- a/cc/config/bp2build.go +++ b/cc/config/bp2build.go @@ -22,14 +22,11 @@ import ( "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 @@ func (m exportedStringVariables) Set(k string, v string) { 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 @@ func (m exportedStringListVariables) asBazel(config android.Config, // 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 @@ func (m exportedStringListDictVariables) Set(k string, v map[string][]string) { 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 @@ func (m exportedStringListDictVariables) asBazel(_ android.Config, _ exportedStr 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 @@ func bazelToolchainVars(config android.Config, vars ...bazelVarExporter) string 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 3118df1f8..4cbf0c6f3 100644 --- a/cc/config/bp2build_test.go +++ b/cc/config/bp2build_test.go @@ -211,15 +211,11 @@ constants = struct( 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 @@ constants = struct( 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 000000000..8d80ccdca --- /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 000000000..23eee59b3 --- /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 000000000..90f78ef7a --- /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) + } +}