RELNOTES: struct.to_proto() converts dict into proto3 text message (map<,>).

Fixes #7896

PiperOrigin-RevId: 243331636
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java b/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java
index 9cf24b0..509a834 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java
@@ -210,7 +210,13 @@
   private void printProtoTextMessage(
       String key, Object value, StringBuilder sb, int indent, Location loc, String container)
       throws EvalException {
-    if (value instanceof ClassObject) {
+    if (value instanceof Map.Entry) {
+      Map.Entry<?, ?> entry = (Map.Entry<?, ?>) value;
+      print(sb, key + " {", indent);
+      printProtoTextMessage("key", entry.getKey(), sb, indent + 1, loc);
+      printProtoTextMessage("value", entry.getValue(), sb, indent + 1, loc);
+      print(sb, "}", indent);
+    } else if (value instanceof ClassObject) {
       print(sb, key + " {", indent);
       printProtoTextMessage((ClassObject) value, sb, indent + 1, loc);
       print(sb, "}", indent);
@@ -228,7 +234,7 @@
     } else {
       throw new EvalException(
           loc,
-          "Invalid text format, expected a struct, a string, a bool, or an int but got a "
+          "Invalid text format, expected a struct, a dict, a string, a bool, or an int but got a "
               + EvalUtils.getDataTypeName(value)
               + " for "
               + container
@@ -246,6 +252,10 @@
         // in the same list but we ignore that for now.
         printProtoTextMessage(key, item, sb, indent, loc, "list element in struct field");
       }
+    } else if (value instanceof SkylarkDict) {
+      for (Map.Entry<?, ?> entry : ((SkylarkDict<?, ?>) value).entrySet()) {
+        printProtoTextMessage(key, entry, sb, indent, loc, "entry of dictionary");
+      }
     } else {
       printProtoTextMessage(key, value, sb, indent, loc, "struct field");
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/StructApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/StructApi.java
index ac79139..10c8b15 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/StructApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/StructApi.java
@@ -38,9 +38,10 @@
       name = "to_proto",
       doc =
           "Creates a text message from the struct parameter. This method only works if all "
-              + "struct elements (recursively) are strings, ints, booleans, other structs or a "
-              + "list of these types. Quotes and new lines in strings are escaped. "
-              + "Keys are iterated in the sorted order. "
+              + "struct elements (recursively) are strings, ints, booleans, "
+              + "other structs or dicts or lists of these types. "
+              + "Quotes and new lines in strings are escaped. "
+              + "Struct keys are iterated in the sorted order. "
               + "Examples:<br><pre class=language-python>"
               + "struct(key=123).to_proto()\n# key: 123\n\n"
               + "struct(key=True).to_proto()\n# key: true\n\n"
@@ -51,7 +52,17 @@
               + "struct(key=[struct(inner_key=1), struct(inner_key=2)]).to_proto()\n"
               + "# key {\n#   inner_key: 1\n# }\n# key {\n#   inner_key: 2\n# }\n\n"
               + "struct(key=struct(inner_key=struct(inner_inner_key='text'))).to_proto()\n"
-              + "# key {\n#    inner_key {\n#     inner_inner_key: \"text\"\n#   }\n# }\n</pre>",
+              + "# key {\n#    inner_key {\n#     inner_inner_key: \"text\"\n#   }\n# }\n\n"
+              + "struct(foo={4: 3, 2: 1}).to_proto()\n"
+              + "# foo: {\n"
+              + "#   key: 4\n"
+              + "#   value: 3\n"
+              + "# }\n"
+              + "# foo: {\n"
+              + "#   key: 2\n"
+              + "#   value: 1\n"
+              + "# }\n"
+              + "</pre>",
       useLocation = true)
   public String toProto(Location loc) throws EvalException;
 
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
index 005d025..8c70a04 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
@@ -860,8 +860,13 @@
   }
 
   private void checkTextMessage(String from, String... lines) throws Exception {
+    String[] strings = lines.clone();
     Object result = evalRuleClassCode(from);
-    assertThat(result).isEqualTo(Joiner.on("\n").join(lines) + "\n");
+    String expect = "";
+    if (strings.length > 0) {
+      expect = Joiner.on("\n").join(lines) + "\n";
+    }
+    assertThat(result).isEqualTo(expect);
   }
 
   @Test
@@ -884,6 +889,7 @@
   @Test
   public void testSimpleTextMessages() throws Exception {
     checkTextMessage("struct(name='value').to_proto()", "name: \"value\"");
+    checkTextMessage("struct(name=[]).to_proto()"); // empty lines
     checkTextMessage("struct(name=['a', 'b']).to_proto()", "name: \"a\"", "name: \"b\"");
     checkTextMessage("struct(name=123).to_proto()", "name: 123");
     checkTextMessage("struct(name=[1, 2, 3]).to_proto()", "name: 1", "name: 2", "name: 3");
@@ -898,6 +904,53 @@
         "}");
     checkTextMessage(
         "struct(a=struct(b=struct(c='c'))).to_proto()", "a {", "  b {", "    c: \"c\"", "  }", "}");
+    // dict to_proto tests
+    checkTextMessage("struct(name={}).to_proto()"); // empty lines
+    checkTextMessage(
+        "struct(name={'a': 'b'}).to_proto()", "name {", "  key: \"a\"", "  value: \"b\"", "}");
+    checkTextMessage(
+        "struct(name={'c': 'd', 'a': 'b'}).to_proto()",
+        "name {",
+        "  key: \"c\"",
+        "  value: \"d\"",
+        "}",
+        "name {",
+        "  key: \"a\"",
+        "  value: \"b\"",
+        "}");
+    checkTextMessage(
+        "struct(x=struct(y={'a': 1})).to_proto()",
+        "x {",
+        "  y {",
+        "    key: \"a\"",
+        "    value: 1",
+        "  }",
+        "}");
+    checkTextMessage(
+        "struct(name={'a': struct(b=1, c=2)}).to_proto()",
+        "name {",
+        "  key: \"a\"",
+        "  value {",
+        "    b: 1",
+        "    c: 2",
+        "  }",
+        "}");
+    checkTextMessage(
+        "struct(name={'a': struct(b={4: 'z', 3: 'y'}, c=2)}).to_proto()",
+        "name {",
+        "  key: \"a\"",
+        "  value {",
+        "    b {",
+        "      key: 4",
+        "      value: \"z\"",
+        "    }",
+        "    b {",
+        "      key: 3",
+        "      value: \"y\"",
+        "    }",
+        "    c: 2",
+        "  }",
+        "}");
   }
 
   @Test
@@ -918,7 +971,7 @@
   @Test
   public void testTextMessageInvalidElementInListStructure() throws Exception {
     checkErrorContains(
-        "Invalid text format, expected a struct, a string, a bool, or "
+        "Invalid text format, expected a struct, a dict, a string, a bool, or "
             + "an int but got a list for list element in struct field 'a'",
         "struct(a=[['b']]).to_proto()");
   }
@@ -926,7 +979,7 @@
   @Test
   public void testTextMessageInvalidStructure() throws Exception {
     checkErrorContains(
-        "Invalid text format, expected a struct, a string, a bool, or an int "
+        "Invalid text format, expected a struct, a dict, a string, a bool, or an int "
             + "but got a function for struct field 'a'",
         "struct(a=rule).to_proto()");
   }