changing the structure of ScopeType to support custom exec behavior. The valid scopes are: `universal`, `project`, `default`, `target`, and values in the form of `exec:<foo>`. Here `foo` could be a specific value of the label to a flag that has the value we need.

PiperOrigin-RevId: 839954752
Change-Id: I8b9250041a40db0405ea8c757ceeaf350926a2ff
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/config/BUILD
index 4a0cd17..085a3a5 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BUILD
@@ -509,6 +509,7 @@
     name = "scope",
     srcs = ["Scope.java"],
     deps = [
+        "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
index d5eb043..067cdae 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
@@ -90,7 +90,7 @@
         .collect(
             toImmutableMap(
                 e -> Label.parseCanonicalUnchecked(e.getKey()),
-                e -> Scope.ScopeType.valueOfIgnoreCase(e.getValue())));
+                e -> new Scope.ScopeType(e.getValue())));
   }
 
   public static BuildOptions getDefaultBuildOptionsForFragments(
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/Scope.java b/src/main/java/com/google/devtools/build/lib/analysis/config/Scope.java
index 08e3960..10b423e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/Scope.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/Scope.java
@@ -13,13 +13,10 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.config;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static java.util.Arrays.stream;
-
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import java.util.Locale;
+import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import javax.annotation.Nullable;
 
 /**
@@ -28,27 +25,34 @@
  */
 public class Scope {
   /** Type of supported scopes. */
-  public enum ScopeType {
-    /** The flag's value never changes except explicitly by a configuraiton transition. * */
-    UNIVERSAL,
-    /** The flag's value resets on exec transitions. * */
-    TARGET,
-    /** The flag resets on targets outside the flag's project. See PROJECT.scl. * */
-    PROJECT,
-    /** Placeholder for flags that don't explicitly specify scope. Shouldn't be set directly. * */
-    DEFAULT;
+  @AutoCodec
+  public record ScopeType(String scopeType) {
+    /** The flag's value never changes except explicitly by a configuration transition. */
+    public static final String UNIVERSAL = "universal";
 
-    /** Returns the enum of a {@code scope = "<string>"} value. */
-    public static ScopeType valueOfIgnoreCase(String scopeType) throws IllegalArgumentException {
-      return ScopeType.valueOf(scopeType.toUpperCase(Locale.ROOT));
+    /** The flag's value resets on exec transitions. */
+    public static final String TARGET = "target";
+
+    /** The flag resets on targets outside the flag's project. See PROJECT.scl. */
+    public static final String PROJECT = "project";
+
+    /** Placeholder for flags that don't explicitly specify scope. Shouldn't be set directly. */
+    public static final String DEFAULT = "default";
+
+    public ScopeType {
+      if (!(scopeType.equals(DEFAULT)
+          || scopeType.equals(UNIVERSAL)
+          || scopeType.equals(TARGET)
+          || scopeType.equals(PROJECT)
+          || scopeType.startsWith("exec:"))) {
+        // TODO: don't let blaze crash for an invalid scope type.
+        throw new IllegalArgumentException("Invalid scope type: " + scopeType);
+      }
     }
 
     /** Which values can a rule's {@code scope} attribute have? */
     public static ImmutableList<String> allowedAttributeValues() {
-      return stream(ScopeType.values())
-          .map(e -> e.name().toLowerCase(Locale.ROOT))
-          .filter(e -> !e.equals("default")) // "default" is an internal value for unset attributes.
-          .collect(toImmutableList());
+      return ImmutableList.of(UNIVERSAL, TARGET, PROJECT);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducer.java b/src/main/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducer.java
index 5982ba9..78efc7a 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducer.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducer.java
@@ -183,7 +183,7 @@
           this.postPlatformProcessedOptions.getScopeTypeMap().get(entry.getKey());
       // scope is null is applicable for cases where a transition applies starlark flags that are
       // not already part of the baseline configuration.
-      if (scopeType == null || scopeType == Scope.ScopeType.PROJECT) {
+      if (scopeType == null || scopeType.scopeType().equals(Scope.ScopeType.PROJECT)) {
         flagsWithIncompleteScopeInfo.add(entry.getKey());
       }
     }
@@ -275,7 +275,7 @@
 
     boolean shouldApplyScopes =
         buildOptionsScopeValue.getFullyResolvedScopes().values().stream()
-            .anyMatch(scope -> scope.getScopeType() == Scope.ScopeType.PROJECT);
+            .anyMatch(scope -> scope.getScopeType().scopeType().equals(Scope.ScopeType.PROJECT));
 
     if (!shouldApplyScopes) {
       return finishConfigurationKeyProcessing(
@@ -342,9 +342,12 @@
       Scope scope = buildOptionsScopeValue.getFullyResolvedScopes().get(flagLabel);
       if (scope == null) {
         Verify.verify(
-            transitionedOptionsWithScopeType.getScopeTypeMap().get(flagLabel)
-                != Scope.ScopeType.PROJECT);
-      } else if (scope.getScopeType() == Scope.ScopeType.PROJECT) {
+            !transitionedOptionsWithScopeType
+                .getScopeTypeMap()
+                .get(flagLabel)
+                .scopeType()
+                .equals(Scope.ScopeType.PROJECT));
+      } else if (scope.getScopeType().scopeType().equals(Scope.ScopeType.PROJECT)) {
         Object flagValue = flagEntry.getValue();
         Object baselineValue = baselineConfiguration.getStarlarkOptions().get(flagLabel);
         if (flagValue != baselineValue && !isInScope(label, scope.getScopeDefinition())) {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/FunctionTransitionUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/FunctionTransitionUtil.java
index d667c0d..c0e4a2d 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/FunctionTransitionUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/FunctionTransitionUtil.java
@@ -236,9 +236,11 @@
       // setting "scope = 'target'".
       return starlarkOptions.entrySet().stream()
           .filter(
-              entry ->
-                  options.getScopeTypeMap().get(entry.getKey()) == Scope.ScopeType.UNIVERSAL
-                      || options.getScopeTypeMap().get(entry.getKey()) == Scope.ScopeType.DEFAULT)
+              entry -> {
+                String scopeType = options.getScopeTypeMap().get(entry.getKey()).scopeType();
+                return scopeType.equals(Scope.ScopeType.UNIVERSAL)
+                    || scopeType.equals(Scope.ScopeType.DEFAULT);
+              })
           .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
@@ -256,9 +258,10 @@
 
     ImmutableMap.Builder<Label, Object> ans = ImmutableMap.builder();
     for (Map.Entry<Label, Object> entry : starlarkOptions.entrySet()) {
-      if (options.getScopeTypeMap().get(entry.getKey()) == Scope.ScopeType.UNIVERSAL) {
+      String scopeType = options.getScopeTypeMap().get(entry.getKey()).scopeType();
+      if (scopeType.equals(Scope.ScopeType.UNIVERSAL)) {
         ans.put(entry);
-      } else if (options.getScopeTypeMap().get(entry.getKey()) == Scope.ScopeType.TARGET) {
+      } else if (scopeType.equals(Scope.ScopeType.TARGET)) {
         // Don't propagate this flag.
       } else if (customPropagatingFlags.contains(entry.getKey().getUnambiguousCanonicalForm())) {
         ans.put(entry);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunction.java
index 5d786d6..ba558ce 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunction.java
@@ -134,7 +134,7 @@
 
     Map<Label, ProjectFilesLookupValue.Key> targetsToSkyKeys = new HashMap<>();
     for (Label starlarkOption : scopes.keySet()) {
-      if (scopes.get(starlarkOption).getScopeType() == Scope.ScopeType.PROJECT) {
+      if (scopes.get(starlarkOption).getScopeType().scopeType().equals(Scope.ScopeType.PROJECT)) {
         targetsToSkyKeys.put(
             starlarkOption, ProjectFilesLookupValue.key(starlarkOption.getPackageIdentifier()));
       }
@@ -184,9 +184,9 @@
         // value when --incompatible_exclude_starlark_flags_from_exec_config is stably enabled
         // and existing rules like skylib's have updated to TARGET.
         || !attrs.isAttributeValueExplicitlySpecified("scope")) {
-      return Scope.ScopeType.DEFAULT;
+      return new Scope.ScopeType(Scope.ScopeType.DEFAULT);
     }
-    return Scope.ScopeType.valueOfIgnoreCase(attrs.get("scope", Type.STRING));
+    return new Scope.ScopeType(attrs.get("scope", Type.STRING));
   }
 
   /**
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducerTest.java b/src/test/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducerTest.java
index 0d3570f..61055f7 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/producers/BuildConfigurationKeyProducerTest.java
@@ -552,9 +552,9 @@
     ImmutableMap<Label, Scope.ScopeType> expectedScopeTypeMap =
         ImmutableMap.of(
             Label.parseCanonicalUnchecked("//flag:foo"),
-            Scope.ScopeType.PROJECT,
+            new Scope.ScopeType(Scope.ScopeType.PROJECT),
             Label.parseCanonicalUnchecked("//flag:bar"),
-            Scope.ScopeType.UNIVERSAL);
+            new Scope.ScopeType(Scope.ScopeType.UNIVERSAL));
     assertThat(result.getOptions().getScopeTypeMap())
         .containsExactlyEntriesIn(expectedScopeTypeMap);
   }
@@ -660,11 +660,11 @@
     ImmutableMap<Label, Scope.ScopeType> expectedScopeTypeMap =
         ImmutableMap.of(
             Label.parseCanonicalUnchecked("//flag:foo"),
-            Scope.ScopeType.PROJECT,
+            new Scope.ScopeType(Scope.ScopeType.PROJECT),
             Label.parseCanonicalUnchecked("//flag:bar"),
-            Scope.ScopeType.UNIVERSAL,
+            new Scope.ScopeType(Scope.ScopeType.UNIVERSAL),
             Label.parseCanonicalUnchecked("//out_of_scope_flag:baz"),
-            Scope.ScopeType.PROJECT);
+            new Scope.ScopeType(Scope.ScopeType.PROJECT));
     assertThat(result.getOptions().getScopeTypeMap())
         .containsExactlyEntriesIn(expectedScopeTypeMap);
   }
@@ -698,9 +698,9 @@
     ImmutableMap<Label, Scope.ScopeType> expectedScopeTypeMap =
         ImmutableMap.of(
             Label.parseCanonicalUnchecked("//flag:foo"),
-            Scope.ScopeType.UNIVERSAL,
+            new Scope.ScopeType(Scope.ScopeType.UNIVERSAL),
             Label.parseCanonicalUnchecked("//flag:bar"),
-            Scope.ScopeType.UNIVERSAL);
+            new Scope.ScopeType(Scope.ScopeType.UNIVERSAL));
     assertThat(result.getOptions().getScopeTypeMap())
         .containsExactlyEntriesIn(expectedScopeTypeMap);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunctionTest.java
index 50170de..ac16ad0 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BuildOptionsScopeFunctionTest.java
@@ -25,7 +25,6 @@
 import com.google.devtools.build.lib.analysis.config.FragmentOptions;
 import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.analysis.config.Scope;
-import com.google.devtools.build.lib.analysis.config.Scope.ScopeType;
 import com.google.devtools.build.lib.analysis.util.AnalysisMock;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -170,19 +169,19 @@
                     ImmutableMap.of(
                         Label.parseCanonical("//test_flags:foo"),
                         new Scope(
-                            Scope.ScopeType.PROJECT,
+                            new Scope.ScopeType(Scope.ScopeType.PROJECT),
                             new Scope.ScopeDefinition(ImmutableSet.of("//my_project/"))),
                         Label.parseCanonical("//test_flags:bar"),
-                        new Scope(ScopeType.UNIVERSAL, null))));
+                        new Scope(new Scope.ScopeType(Scope.ScopeType.UNIVERSAL), null))));
 
     // verify that the BuildOptionsScopeValue.getResolvedBuildOptionsWithScopeTypes() has the
     // correct ScopeType map for all flags.
     assertThat(buildOptionsScopeValue.getResolvedBuildOptionsWithScopeTypes().getScopeTypeMap())
         .containsExactly(
             Label.parseCanonical("//test_flags:foo"),
-            Scope.ScopeType.PROJECT,
+            new Scope.ScopeType(Scope.ScopeType.PROJECT),
             Label.parseCanonical("//test_flags:bar"),
-            Scope.ScopeType.UNIVERSAL);
+            new Scope.ScopeType(Scope.ScopeType.UNIVERSAL));
   }
 
   @Test
@@ -223,7 +222,7 @@
                 .equals(
                     ImmutableMap.of(
                         Label.parseCanonical("//test_flags:foo"),
-                        new Scope(Scope.ScopeType.PROJECT, null))));
+                        new Scope(new Scope.ScopeType(Scope.ScopeType.PROJECT), null))));
   }
 
   private BuildOptionsScopeValue executeFunction(BuildOptionsScopeValue.Key key) throws Exception {