Follow unconfigured `alias`es during options parsing

Fixes #20582

RELNOTES: Starlark command-line flags can now be referred to through `alias` targets.

Closes #22192.

PiperOrigin-RevId: 629865954
Change-Id: I6215c8484ddc08e75507191bfa1eb5bc709c5fc6
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java b/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java
index 7461ae4..976cf73 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java
@@ -17,6 +17,7 @@
 import static com.google.devtools.build.lib.analysis.config.CoreOptionConverters.BUILD_SETTING_CONVERTERS;
 import static com.google.devtools.build.lib.packages.RuleClass.Builder.STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME;
 import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static java.util.stream.Collectors.joining;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -27,7 +28,7 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.packages.BuildSetting;
-import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.BuildType;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.Type;
 import com.google.devtools.build.lib.packages.Types;
@@ -38,10 +39,13 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.SequencedSet;
 import java.util.TreeMap;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 
 /**
@@ -282,7 +286,7 @@
   }
 
   /**
-   * Returns the given build setting's {@link Target}.
+   * Returns the given build setting's {@link Target}, following (unconfigured) aliases if needed.
    *
    * @return the target, or null if the {@link BuildSettingLoader} needs to do more work to retrieve
    *     the target
@@ -293,23 +297,71 @@
       return buildSettings.get(targetToBuild);
     }
 
-    Target buildSetting;
-    try {
-      buildSetting = buildSettingLoader.loadBuildSetting(targetToBuild);
-      if (buildSetting == null) {
-        return null;
+    Target target;
+    String targetToLoadNext = targetToBuild;
+    SequencedSet<Label> aliasChain = new LinkedHashSet<>();
+    while (true) {
+      try {
+        target = buildSettingLoader.loadBuildSetting(targetToLoadNext);
+        if (target == null) {
+          return null;
+        }
+      } catch (InterruptedException | TargetParsingException e) {
+        Thread.currentThread().interrupt();
+        throw new OptionsParsingException(
+            "Error loading option " + targetToBuild + ": " + e.getMessage(), targetToBuild, e);
       }
-    } catch (InterruptedException | TargetParsingException e) {
-      Thread.currentThread().interrupt();
+      if (!aliasChain.add(target.getLabel())) {
+        throw new OptionsParsingException(
+            String.format(
+                "Failed to load build setting '%s' due to a cycle in alias chain: %s",
+                targetToBuild,
+                formatAliasChain(Stream.concat(aliasChain.stream(), Stream.of(target.getLabel())))),
+            targetToBuild);
+      }
+      if (target.getAssociatedRule() == null) {
+        throw new OptionsParsingException(
+            String.format("Unrecognized option: %s", formatAliasChain(aliasChain.stream())),
+            targetToBuild);
+      }
+      if (target.getAssociatedRule().isBuildSetting()) {
+        break;
+      }
+      // Follow the unconfigured values of aliases.
+      if (target.getAssociatedRule().getRuleClass().equals("alias")) {
+        targetToLoadNext =
+            switch (target.getAssociatedRule().getAttr("actual")) {
+              case Label label -> label.getUnambiguousCanonicalForm();
+              case BuildType.SelectorList<?> ignored ->
+                  throw new OptionsParsingException(
+                      String.format(
+                          "Failed to load build setting '%s' as it resolves to an alias with an"
+                              + " actual value that uses select(): %s. This is not supported as"
+                              + " build settings are needed to determine the configuration the"
+                              + " select is evaluated in.",
+                          targetToBuild, formatAliasChain(aliasChain.stream())),
+                      targetToBuild);
+              case null, default ->
+                  throw new IllegalStateException(
+                      String.format(
+                          "Alias target '%s' with 'actual' attr value not equals to a label or a"
+                              + " selectorlist",
+                          target.getLabel()));
+            };
+        continue;
+      }
       throw new OptionsParsingException(
-          "Error loading option " + targetToBuild + ": " + e.getMessage(), targetToBuild, e);
+          String.format("Unrecognized option: %s", formatAliasChain(aliasChain.stream())),
+          targetToBuild);
     }
-    Rule associatedRule = buildSetting.getAssociatedRule();
-    if (associatedRule == null || associatedRule.getRuleClassObject().getBuildSetting() == null) {
-      throw new OptionsParsingException("Unrecognized option: " + targetToBuild, targetToBuild);
-    }
-    buildSettings.put(targetToBuild, buildSetting);
-    return buildSetting;
+    ;
+
+    buildSettings.put(targetToBuild, target);
+    return target;
+  }
+
+  private static String formatAliasChain(Stream<Label> aliasChain) {
+    return aliasChain.map(Label::getCanonicalForm).collect(joining(" -> "));
   }
 
   @VisibleForTesting
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java
index e36f782..3375361 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java
@@ -567,4 +567,146 @@
         .hasMessageThat()
         .contains("//test:all: user-defined flags must reference exactly one target");
   }
+
+  @Test
+  public void flagIsAlias() throws Exception {
+    scratch.file(
+        "test/build_setting.bzl",
+        """
+        string_flag = rule(
+            implementation = lambda ctx: [],
+            build_setting = config.string(flag = True),
+        )
+        """);
+    scratch.file(
+        "test/BUILD",
+        """
+        load("//test:build_setting.bzl", "string_flag")
+
+        alias(
+            name = "one",
+            actual = "//test/pkg:two",
+        )
+
+        string_flag(
+            name = "three",
+            build_setting_default = "",
+        )
+        """);
+    scratch.file(
+        "test/pkg/BUILD",
+        """
+        alias(
+            name = "two",
+            actual = "//test:three",
+        )
+        """);
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:one=one --//test/pkg:two=two");
+
+    assertThat(result.getStarlarkOptions()).containsExactly("//test:three", "two");
+  }
+
+  @Test
+  public void flagIsAlias_cycle() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        """
+        alias(
+            name = "one",
+            actual = "//test/pkg:two",
+        )
+
+        alias(
+            name = "three",
+            actual = ":one",
+        )
+        """);
+    scratch.file(
+        "test/pkg/BUILD",
+        """
+        alias(
+            name = "two",
+            actual = "//test:three",
+        )
+        """);
+
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:one=one"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to load build setting '//test:one' due to a cycle in alias chain: //test:one"
+                + " -> //test/pkg:two -> //test:three -> //test:one");
+  }
+
+  @Test
+  public void flagIsAlias_usesSelect() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        """
+        alias(
+            name = "one",
+            actual = "//test/pkg:two",
+        )
+
+        alias(
+            name = "three",
+            actual = ":one",
+        )
+        """);
+    scratch.file(
+        "test/pkg/BUILD",
+        """
+        alias(
+            name = "two",
+            actual = select({"//conditions:default": "//test:three"}),
+        )
+        """);
+
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:one=one"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to load build setting '//test:one' as it resolves to an alias with an actual"
+                + " value that uses select(): //test:one -> //test/pkg:two. This is not supported"
+                + " as build settings are needed to determine the configuration the select is"
+                + " evaluated in.");
+  }
+
+  @Test
+  public void flagIsAlias_resolvesToNonBuildSettingTarget() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        """
+        alias(
+            name = "one",
+            actual = "//test/pkg:two",
+        )
+
+        genrule(
+            name = "three",
+            outs = ["out"],
+            cmd = "echo hello > $@",
+        )
+        """);
+    scratch.file(
+        "test/pkg/BUILD",
+        """
+        alias(
+            name = "two",
+            actual = "//test:three",
+        )
+        """);
+
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:one=one"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Unrecognized option: //test:one -> //test/pkg:two -> //test:three");
+  }
 }