Add the LABEL_KEYED_STRING_DICT type for attributes.

This enables both native and Skylark rules to declare attributes which
have labels/Targets as keys, and have string values.

--
PiperOrigin-RevId: 148365033
MOS_MIGRATED_REVID=148365033
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AttributeFormatter.java b/src/main/java/com/google/devtools/build/lib/packages/AttributeFormatter.java
index 44623f7..4a1c283 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/AttributeFormatter.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/AttributeFormatter.java
@@ -17,6 +17,7 @@
 import static com.google.devtools.build.lib.packages.BuildType.FILESET_ENTRY_LIST;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL_DICT_UNARY;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL_KEYED_STRING_DICT;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
 import static com.google.devtools.build.lib.packages.BuildType.LICENSE;
 import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL;
@@ -45,6 +46,7 @@
 import com.google.devtools.build.lib.query2.proto.proto2api.Build.Attribute.SelectorEntry.Builder;
 import com.google.devtools.build.lib.query2.proto.proto2api.Build.Attribute.Tristate;
 import com.google.devtools.build.lib.query2.proto.proto2api.Build.LabelDictUnaryEntry;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.LabelKeyedStringDictEntry;
 import com.google.devtools.build.lib.query2.proto.proto2api.Build.LabelListDictEntry;
 import com.google.devtools.build.lib.query2.proto.proto2api.Build.StringDictEntry;
 import com.google.devtools.build.lib.query2.proto.proto2api.Build.StringDictUnaryEntry;
@@ -61,7 +63,15 @@
 
   private static final ImmutableSet<Type<?>> depTypes =
       ImmutableSet.<Type<?>>of(
-          STRING, LABEL, OUTPUT, STRING_LIST, LABEL_LIST, OUTPUT_LIST, DISTRIBUTIONS);
+          STRING,
+          LABEL,
+          OUTPUT,
+          STRING_LIST,
+          LABEL_LIST,
+          LABEL_DICT_UNARY,
+          LABEL_KEYED_STRING_DICT,
+          OUTPUT_LIST,
+          DISTRIBUTIONS);
 
   private static final ImmutableSet<Type<?>> noDepTypes =
       ImmutableSet.<Type<?>>of(NODEP_LABEL_LIST, NODEP_LABEL);
@@ -230,6 +240,15 @@
                 .setValue(dictEntry.getValue().toString());
         builder.addLabelDictUnaryValue(entry);
       }
+    } else if (type == LABEL_KEYED_STRING_DICT) {
+      Map<Label, String> dict = (Map<Label, String>) value;
+      for (Map.Entry<Label, String> dictEntry : dict.entrySet()) {
+        LabelKeyedStringDictEntry.Builder entry =
+            LabelKeyedStringDictEntry.newBuilder()
+                .setKey(dictEntry.getKey().toString())
+                .setValue(dictEntry.getValue());
+        builder.addLabelKeyedStringDictValue(entry);
+      }
     } else if (type == FILESET_ENTRY_LIST) {
       List<FilesetEntry> filesetEntries = (List<FilesetEntry>) value;
       for (FilesetEntry filesetEntry : filesetEntries) {
@@ -302,6 +321,8 @@
 
     void addLabelDictUnaryValue(LabelDictUnaryEntry.Builder builder);
 
+    void addLabelKeyedStringDictValue(LabelKeyedStringDictEntry.Builder builder);
+
     void addLabelListDictValue(LabelListDictEntry.Builder builder);
 
     void addIntListValue(int i);
@@ -362,6 +383,11 @@
     }
 
     @Override
+    public void addLabelKeyedStringDictValue(LabelKeyedStringDictEntry.Builder builder) {
+      attributeBuilder.addLabelKeyedStringDictValue(builder);
+    }
+
+    @Override
     public void addLabelListDictValue(LabelListDictEntry.Builder builder) {
       attributeBuilder.addLabelListDictValue(builder);
     }
@@ -488,6 +514,11 @@
     }
 
     @Override
+    public void addLabelKeyedStringDictValue(LabelKeyedStringDictEntry.Builder builder) {
+      selectorEntryBuilder.addLabelKeyedStringDictValue(builder);
+    }
+
+    @Override
     public void addLabelListDictValue(LabelListDictEntry.Builder builder) {
       selectorEntryBuilder.addLabelListDictValue(builder);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/packages/BuildType.java b/src/main/java/com/google/devtools/build/lib/packages/BuildType.java
index 051b7e0..263fe63 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/BuildType.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/BuildType.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.lib.packages;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -22,6 +23,7 @@
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.packages.License.DistributionType;
 import com.google.devtools.build.lib.packages.License.LicenseParsingException;
+import com.google.devtools.build.lib.syntax.Printer;
 import com.google.devtools.build.lib.syntax.Runtime;
 import com.google.devtools.build.lib.syntax.SelectorValue;
 import com.google.devtools.build.lib.syntax.Type;
@@ -29,6 +31,7 @@
 import com.google.devtools.build.lib.syntax.Type.DictType;
 import com.google.devtools.build.lib.syntax.Type.LabelClass;
 import com.google.devtools.build.lib.syntax.Type.ListType;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -55,6 +58,11 @@
   public static final DictType<String, Label> LABEL_DICT_UNARY = DictType.create(
       Type.STRING, LABEL);
   /**
+   * The type of a dictionary keyed by {@linkplain #LABEL labels} with string values.
+   */
+  public static final DictType<Label, String> LABEL_KEYED_STRING_DICT =
+      LabelKeyedDictType.create(Type.STRING);
+  /**
    *  The type of a list of {@linkplain #LABEL labels}.
    */
   public static final ListType<Label> LABEL_LIST = ListType.create(LABEL);
@@ -247,6 +255,70 @@
   }
 
   /**
+   * Dictionary type specialized for label keys, which is able to detect collisions caused by the
+   * fact that labels have multiple equivalent representations in Skylark code.
+   */
+  private static class LabelKeyedDictType<ValueT> extends DictType<Label, ValueT> {
+    private LabelKeyedDictType(Type<ValueT> valueType) {
+      super(LABEL, valueType, LabelClass.DEPENDENCY);
+    }
+
+    public static <ValueT> LabelKeyedDictType<ValueT> create(Type<ValueT> valueType) {
+      Preconditions.checkArgument(
+          valueType.getLabelClass() == LabelClass.NONE
+          || valueType.getLabelClass() == LabelClass.DEPENDENCY,
+          "Values associated with label keys must not be labels themselves.");
+      return new LabelKeyedDictType<>(valueType);
+    }
+
+    @Override
+    public Map<Label, ValueT> convert(Object x, Object what, Object context)
+        throws ConversionException {
+      Map<Label, ValueT> result = super.convert(x, what, context);
+      // The input is known to be a map because super.convert succeded; otherwise, a
+      // ConversionException would have been thrown.
+      Map<?, ?> input = (Map<?, ?>) x;
+
+      if (input.size() == result.size()) {
+        // No collisions found. Exit early.
+        return result;
+      }
+      // Look for collisions in order to produce a nicer error message.
+      Map<Label, List<Object>> convertedFrom = new LinkedHashMap<>();
+      for (Object original : input.keySet()) {
+        Label label = LABEL.convert(original, what, context);
+        if (!convertedFrom.containsKey(label)) {
+          convertedFrom.put(label, new ArrayList<Object>());
+        }
+        convertedFrom.get(label).add(original);
+      }
+      StringBuilder errorMessage = new StringBuilder();
+      errorMessage.append("duplicate labels");
+      if (what != null) {
+        errorMessage.append(" in ").append(what);
+      }
+      errorMessage.append(':');
+      boolean isFirstEntry = true;
+      for (Map.Entry<Label, List<Object>> entry : convertedFrom.entrySet()) {
+        if (entry.getValue().size() == 1) {
+          continue;
+        }
+        if (isFirstEntry) {
+          isFirstEntry = false;
+        } else {
+          errorMessage.append(',');
+        }
+        errorMessage.append(' ');
+        errorMessage.append(entry.getKey());
+        errorMessage.append(" (as ");
+        Printer.write(errorMessage, entry.getValue());
+        errorMessage.append(')');
+      }
+      throw new ConversionException(errorMessage.toString());
+    }
+  }
+
+  /**
    * Like Label, LicenseType is a derived type, which is declared specially
    * in order to allow syntax validation. It represents the licenses, as
    * described in {@ref License}.
diff --git a/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java b/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
index c34ad2e..7c0335d 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
@@ -18,6 +18,7 @@
 import static com.google.devtools.build.lib.packages.BuildType.FILESET_ENTRY_LIST;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL_DICT_UNARY;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL_KEYED_STRING_DICT;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
 import static com.google.devtools.build.lib.packages.BuildType.LICENSE;
 import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL;
@@ -68,6 +69,7 @@
           .put(TRISTATE, Discriminator.TRISTATE)
           .put(INTEGER_LIST, Discriminator.INTEGER_LIST)
           .put(STRING_DICT_UNARY, Discriminator.STRING_DICT_UNARY)
+          .put(LABEL_KEYED_STRING_DICT, Discriminator.LABEL_KEYED_STRING_DICT)
           .build();
 
   /** Returns the {@link Discriminator} value corresponding to the provided {@link Type}. */
diff --git a/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java
index 317888d..66e1182 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java
@@ -432,7 +432,8 @@
     if (attrType == Type.STRING_DICT
         || attrType == Type.STRING_DICT_UNARY
         || attrType == Type.STRING_LIST_DICT
-        || attrType == BuildType.LABEL_DICT_UNARY) {
+        || attrType == BuildType.LABEL_DICT_UNARY
+        || attrType == BuildType.LABEL_KEYED_STRING_DICT) {
       Map<Object, Object> mergedDict = new HashMap<>();
       for (Object possibleValue : possibleValues) {
         Map<Object, Object> stringDict = (Map<Object, Object>) possibleValue;
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java
index 08113b6..b28b08e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java
@@ -922,6 +922,158 @@
       };
 
   @SkylarkSignature(
+    name = "label_keyed_string_dict",
+    doc =
+        "Creates an attribute which is a <a href=\"dict.html\">dict</a>. Its keys are type "
+            + "<a href=\"Target.html\">Target</a> and are specified by the label keys of the "
+            + "input dict. Its values are <a href=\"string.html\">strings</a>. See "
+            + "<a href=\"attr.html#label\">label</a> for more information.",
+    objectType = SkylarkAttr.class,
+    returnType = Descriptor.class,
+    parameters = {
+      @Param(
+        name = DEFAULT_ARG,
+        type = SkylarkDict.class,
+        callbackEnabled = true,
+        defaultValue = "{}",
+        named = true,
+        positional = false,
+        doc =
+            DEFAULT_DOC
+                + " Use the <a href=\"globals.html#Label\"><code>Label</code></a> function to "
+                + "specify default values ex:</p>"
+                + "<code>attr.label_keyed_string_dict(default = "
+                + "{ Label(\"//a:b\"): \"value\", Label(\"//a:c\"): \"string\" })</code>"
+      ),
+      @Param(
+        name = ALLOW_FILES_ARG, // bool or FileType filter
+        defaultValue = "None",
+        named = true,
+        positional = false,
+        doc = ALLOW_FILES_DOC
+      ),
+      @Param(
+        name = ALLOW_RULES_ARG,
+        type = SkylarkList.class,
+        generic1 = String.class,
+        noneable = true,
+        defaultValue = "None",
+        named = true,
+        positional = false,
+        doc = ALLOW_RULES_DOC
+      ),
+      @Param(
+        name = PROVIDERS_ARG,
+        type = SkylarkList.class,
+        defaultValue = "[]",
+        named = true,
+        positional = false,
+        doc = PROVIDERS_DOC
+      ),
+      @Param(
+        name = FLAGS_ARG,
+        type = SkylarkList.class,
+        generic1 = String.class,
+        defaultValue = "[]",
+        named = true,
+        positional = false,
+        doc = FLAGS_DOC
+      ),
+      @Param(
+        name = MANDATORY_ARG,
+        type = Boolean.class,
+        defaultValue = "False",
+        named = true,
+        positional = false,
+        doc = MANDATORY_DOC
+      ),
+      @Param(
+        name = NON_EMPTY_ARG,
+        type = Boolean.class,
+        defaultValue = "False",
+        named = true,
+        positional = false,
+        doc = NON_EMPTY_DOC
+      ),
+      @Param(
+        name = ALLOW_EMPTY_ARG,
+        type = Boolean.class,
+        defaultValue = "True",
+        doc = ALLOW_EMPTY_DOC
+      ),
+      @Param(
+        name = CONFIGURATION_ARG,
+        type = Object.class,
+        noneable = true,
+        defaultValue = "None",
+        named = true,
+        positional = false,
+        doc = CONFIGURATION_DOC
+      ),
+      @Param(
+        name = ASPECTS_ARG,
+        type = SkylarkList.class,
+        generic1 = SkylarkAspect.class,
+        defaultValue = "[]",
+        named = true,
+        positional = false,
+        doc = ASPECTS_ARG_DOC
+      )
+    },
+    useAst = true,
+    useEnvironment = true
+  )
+  private static BuiltinFunction labelKeyedStringDict =
+      new BuiltinFunction("label_keyed_string_dict") {
+        public Descriptor invoke(
+            Object defaultList,
+            Object allowFiles,
+            Object allowRules,
+            SkylarkList<?> providers,
+            SkylarkList<?> flags,
+            Boolean mandatory,
+            Boolean nonEmpty,
+            Boolean allowEmpty,
+            Object cfg,
+            SkylarkList<?> aspects,
+            FuncallExpression ast,
+            Environment env)
+            throws EvalException {
+          env.checkLoadingOrWorkspacePhase("attr.label_keyed_string_dict", ast.getLocation());
+          SkylarkDict<String, Object> kwargs =
+              EvalUtils.<String, Object>optionMap(
+                  env,
+                  DEFAULT_ARG,
+                  defaultList,
+                  ALLOW_FILES_ARG,
+                  allowFiles,
+                  ALLOW_RULES_ARG,
+                  allowRules,
+                  PROVIDERS_ARG,
+                  providers,
+                  FLAGS_ARG,
+                  flags,
+                  MANDATORY_ARG,
+                  mandatory,
+                  NON_EMPTY_ARG,
+                  nonEmpty,
+                  ALLOW_EMPTY_ARG,
+                  allowEmpty,
+                  CONFIGURATION_ARG,
+                  cfg);
+          try {
+            Attribute.Builder<?> attribute =
+                createAttribute(BuildType.LABEL_KEYED_STRING_DICT, kwargs, ast, env);
+            ImmutableList<SkylarkAspect> skylarkAspects =
+                ImmutableList.copyOf(aspects.getContents(SkylarkAspect.class, "aspects"));
+            return new Descriptor(attribute, skylarkAspects);
+          } catch (EvalException e) {
+            throw new EvalException(ast.getLocation(), e.getMessage(), e);
+          }
+        }
+      };
+
+  @SkylarkSignature(
     name = "bool",
     doc = "Creates an attribute of type bool.",
     objectType = SkylarkAttr.class,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java
index 5414bd9..def0452 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java
@@ -344,6 +344,16 @@
           || (type == BuildType.LABEL && a.hasSplitConfigurationTransition())) {
         List<?> allPrereq = ruleContext.getPrerequisites(a.getName(), Mode.DONT_CHECK);
         attrBuilder.put(skyname, SkylarkList.createImmutable(allPrereq));
+      } else if (type == BuildType.LABEL_KEYED_STRING_DICT) {
+        ImmutableMap.Builder<TransitiveInfoCollection, String> builder =
+            new ImmutableMap.Builder<>();
+        Map<Label, String> original = BuildType.LABEL_KEYED_STRING_DICT.cast(val);
+        List<? extends TransitiveInfoCollection> allPrereq =
+            ruleContext.getPrerequisites(a.getName(), Mode.DONT_CHECK);
+        for (TransitiveInfoCollection prereq : allPrereq) {
+          builder.put(prereq, original.get(prereq.getLabel()));
+        }
+        attrBuilder.put(skyname, SkylarkType.convertToSkylark(builder.build(), null));
       } else if (type == BuildType.LABEL_DICT_UNARY) {
         Map<Label, TransitiveInfoCollection> prereqsByLabel = new LinkedHashMap<>();
         for (TransitiveInfoCollection target
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Type.java b/src/main/java/com/google/devtools/build/lib/syntax/Type.java
index 387794b..fc89927 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Type.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Type.java
@@ -475,7 +475,7 @@
       return new DictType<>(keyType, valueType, labelClass);
     }
 
-    private DictType(Type<KeyT> keyType, Type<ValueT> valueType, LabelClass labelClass) {
+    protected DictType(Type<KeyT> keyType, Type<ValueT> valueType, LabelClass labelClass) {
       this.keyType = keyType;
       this.valueType = valueType;
       this.labelClass = labelClass;
@@ -509,8 +509,7 @@
     public Map<KeyT, ValueT> convert(Object x, Object what, Object context)
         throws ConversionException {
       if (!(x instanceof Map<?, ?>)) {
-        throw new ConversionException(String.format(
-            "Expected a map for dictionary but got a %s", x.getClass().getName()));
+        throw new ConversionException(this, x, what);
       }
       // Order the keys so the return value will be independent of insertion order.
       Map<KeyT, ValueT> result = new TreeMap<>();