Allow *allowMultiple* options to be *set* by Starlark transitions.

Specifically helpful for --define.

Work towards #5574

RELNOTES: None.
PiperOrigin-RevId: 242736849
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
index 07f2649..da86316 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
@@ -33,12 +33,16 @@
 import com.google.devtools.build.lib.syntax.Runtime;
 import com.google.devtools.build.lib.syntax.Runtime.NoneType;
 import com.google.devtools.build.lib.syntax.SkylarkDict;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
 import com.google.devtools.common.options.OptionDefinition;
 import com.google.devtools.common.options.OptionsParser;
 import com.google.devtools.common.options.OptionsParsingException;
 import java.lang.reflect.Field;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -175,7 +179,33 @@
           Field field = optionInfo.getDefinition().getField();
           FragmentOptions options = buildOptions.get(optionInfo.getOptionClass());
           Object optionValue = field.get(options);
-
+          if (optionInfo.getDefinition().allowsMultiple()) {
+            List<?> optionValueList = (List<?>) optionValue;
+            if (optionValueList.isEmpty()) {
+              optionValue = MutableList.empty();
+            } else {
+              if (optionValueList.get(0) instanceof Map.Entry) {
+                SkylarkDict<String, String> valueDict = SkylarkDict.withMutability(mutability);
+                for (Map.Entry singleValue : (List<Map.Entry>) optionValueList) {
+                  valueDict.put(
+                      singleValue.getKey().toString(),
+                      singleValue.getValue().toString(),
+                      starlarkTransition.getLocationForErrorReporting(),
+                      mutability);
+                }
+                optionValue = valueDict;
+              }
+            }
+          } else {
+            if (optionValue instanceof Map.Entry) {
+              SkylarkDict<String, String> valueDict = SkylarkDict.withMutability(mutability);
+              valueDict.put(
+                  ((Map.Entry) optionValue).getKey().toString(),
+                  ((Map.Entry) optionValue).getValue().toString(),
+                  starlarkTransition.getLocationForErrorReporting(),
+                  mutability);
+            }
+          }
           dict.put(optionKey, optionValue == null ? Runtime.NONE : optionValue, null, mutability);
         } catch (IllegalAccessException e) {
           // These exceptions should not happen, but if they do, throw a RuntimeException.
@@ -254,17 +284,35 @@
           OptionDefinition def = optionInfo.getDefinition();
           Field field = def.getField();
           FragmentOptions options = buildOptions.get(optionInfo.getOptionClass());
-          if (optionValue == null || def.getType().isInstance(optionValue)) {
-            field.set(options, optionValue);
-          } else if (optionValue instanceof String) {
-            field.set(options, def.getConverter().convert((String) optionValue));
+
+          if (!def.allowsMultiple()) {
+            if (optionValue == null || def.getType().isInstance(optionValue)) {
+              field.set(options, optionValue);
+            } else if (optionValue instanceof String) {
+              field.set(options, def.getConverter().convert((String) optionValue));
+            } else {
+              throw new EvalException(
+                  starlarkTransition.getLocationForErrorReporting(),
+                  "Invalid value type for option '" + optionName + "'");
+            }
           } else {
-            throw new EvalException(
-                starlarkTransition.getLocationForErrorReporting(),
-                "Invalid value type for option '" + optionName + "'");
+            SkylarkList rawValues =
+                optionValue instanceof SkylarkList
+                    ? (SkylarkList) optionValue
+                    : SkylarkList.createImmutable(Collections.singletonList(optionValue));
+            List<Object> allValues = new ArrayList<>(rawValues.size());
+            for (Object singleValue : rawValues) {
+              if (singleValue instanceof String) {
+                allValues.add(def.getConverter().convert((String) singleValue));
+              } else {
+                allValues.add(singleValue);
+              }
+            }
+            field.set(options, ImmutableList.copyOf(allValues));
           }
         } catch (IllegalAccessException e) {
-          throw new RuntimeException(
+          throw new EvalException(
+              starlarkTransition.getLocationForErrorReporting(),
               "IllegalAccess for option " + optionName + ": " + e.getMessage());
         } catch (OptionsParsingException e) {
           throw new EvalException(
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
index c5bf9d0..ad5ab6c 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration.EmptyToNullLabelConverter;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
@@ -28,9 +29,12 @@
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.common.options.Converters;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionDocumentationCategory;
 import com.google.devtools.common.options.OptionEffectTag;
+import java.util.List;
+import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -54,6 +58,16 @@
         effectTags = {OptionEffectTag.NO_OP},
         help = "An option that is sometimes set to null.")
     public Label nullable;
+
+    @Option(
+        name = "allow_multiple",
+        converter = Converters.AssignmentConverter.class,
+        defaultValue = "",
+        allowMultiple = true,
+        documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
+        effectTags = {OptionEffectTag.CHANGES_INPUTS, OptionEffectTag.AFFECTS_OUTPUTS},
+        help = "Each --define option specifies an assignment for a build variable.")
+    public List<Map.Entry<String, String>> allowMultiple;
   }
 
   /** Loads a new {link @DummyTestFragment} instance. */
@@ -854,4 +868,38 @@
     assertThat(configuration.getOptions().get(TestOptions.class).testArguments)
         .containsExactly("post-transition");
   }
+
+  @Test
+  public void testWriteNativeOption_allowMultipleOption() throws Exception {
+    writeWhitelistFile();
+    scratch.file(
+        "test/transitions.bzl",
+        "def _impl(settings, attr):",
+        "  return {",
+        "  '//command_line_option:allow_multiple': ['APRIL=SHOWERS', 'MAY=FLOWERS'],",
+        "  }",
+        "my_transition = transition(implementation = _impl, inputs = [],",
+        "  outputs = ['//command_line_option:allow_multiple'])");
+    scratch.file(
+        "test/rules.bzl",
+        "load('//test:transitions.bzl', 'my_transition')",
+        "def _impl(ctx):",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  cfg = my_transition,",
+        "  attrs = {",
+        "    '_whitelist_function_transition': attr.label(",
+        "        default = '//tools/whitelists/function_transition_whitelist',",
+        "    ),",
+        "  })");
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'test')");
+
+    useConfiguration("--allow_multiple=APRIL=FOOLS");
+
+    BuildConfiguration configuration = getConfiguration(getConfiguredTarget("//test"));
+    assertThat(configuration.getOptions().get(DummyTestOptions.class).allowMultiple)
+        .containsExactly(
+            Maps.immutableEntry("APRIL", "SHOWERS"), Maps.immutableEntry("MAY", "FLOWERS"));
+  }
 }
diff --git a/src/test/shell/bazel/whitelist_test.sh b/src/test/shell/bazel/whitelist_test.sh
index e3c2b12..8c4ea1b 100755
--- a/src/test/shell/bazel/whitelist_test.sh
+++ b/src/test/shell/bazel/whitelist_test.sh
@@ -99,7 +99,7 @@
     --noimplicit_deps --experimental_starlark_config_transitions \
     >& $TEST_log || fail "failed to query //vinegar"
   expect_log "@secret_ingredient//hotsauce"
-  expect_log "test_arg:\[hotlanta\] -> \[\[\"tapatio\"\]\]"
+  expect_log "test_arg:\[hotlanta\] -> \[\[tapatio\]\]"
 
 }