Remove `values` constraint from command-line aspects parameters

Aspect parameters of type string are required to have `values` constraint to specify its set of allowed values. This CL removes this requirement for command-line aspects and leave it for rule-propagated aspects.

PiperOrigin-RevId: 416246364
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java
index cd2b5fb..fa7c661 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java
@@ -59,6 +59,7 @@
 import com.google.devtools.build.lib.events.EventKind;
 import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.packages.AllowlistChecker;
+import com.google.devtools.build.lib.packages.AspectsListBuilder.AspectDetails;
 import com.google.devtools.build.lib.packages.Attribute;
 import com.google.devtools.build.lib.packages.Attribute.StarlarkComputedDefaultTemplate;
 import com.google.devtools.build.lib.packages.AttributeValueSource;
@@ -634,15 +635,13 @@
         hasDefault = false; // isValueSet() is always true for attr.string.
       }
       if (!Attribute.isImplicit(nativeName) && !Attribute.isLateBound(nativeName)) {
-        if (!attribute.checkAllowedValues() || attribute.getType() != Type.STRING) {
+        if (attribute.getType() != Type.STRING) {
           throw Starlark.errorf(
-              "Aspect parameter attribute '%s' must have type 'string' and use the 'values'"
-                  + " restriction.",
-              nativeName);
+              "Aspect parameter attribute '%s' must have type 'string'.", nativeName);
         }
         if (!hasDefault) {
           requiredParams.add(nativeName);
-        } else {
+        } else if (attribute.checkAllowedValues()) {
           PredicateWithMessage<Object> allowed = attribute.getAllowedValues();
           Object defaultVal = attribute.getDefaultValue(null);
           if (!allowed.apply(defaultVal)) {
@@ -762,20 +761,7 @@
         throw new EvalException("Invalid rule class hasn't been exported by a bzl file");
       }
 
-      for (Attribute attribute : ruleClass.getAttributes()) {
-        // TODO(dslomov): If a Starlark parameter extractor is specified for this aspect, its
-        // attributes may not be required.
-        for (Map.Entry<String, ImmutableSet<String>> attrRequirements :
-            attribute.getRequiredAspectParameters().entrySet()) {
-          for (String required : attrRequirements.getValue()) {
-            if (!ruleClass.hasAttr(required, Type.STRING)) {
-              throw Starlark.errorf(
-                  "Aspect %s requires rule %s to specify attribute '%s' with type string.",
-                  attrRequirements.getKey(), ruleClass.getName(), required);
-            }
-          }
-        }
-      }
+      validateRulePropagatedAspects(ruleClass);
 
       BuildLangTypedAttributeValuesMap attributeValues =
           new BuildLangTypedAttributeValuesMap(kwargs);
@@ -799,6 +785,40 @@
       return Starlark.NONE;
     }
 
+    private static void validateRulePropagatedAspects(RuleClass ruleClass) throws EvalException {
+      for (Attribute attribute : ruleClass.getAttributes()) {
+        for (AspectDetails<?> aspect : attribute.getAspectsDetails()) {
+          ImmutableSet<String> requiredAspectParameters = aspect.getRequiredParameters();
+          for (Attribute aspectAttribute : aspect.getAspectAttributes()) {
+            String aspectAttrName = aspectAttribute.getPublicName();
+            Type<?> aspectAttrType = aspectAttribute.getType();
+
+            // When propagated from a rule, explicit aspect attributes must be of type string and
+            // they must have `values` restriction.
+            if (!aspectAttribute.isImplicit() && !aspectAttribute.isLateBound()) {
+              if ((aspectAttrType != Type.STRING && aspectAttrType != Type.INTEGER)
+                  || !aspectAttribute.checkAllowedValues()) {
+                throw Starlark.errorf(
+                    "Aspect %s: Aspect parameter attribute '%s' must have type 'string'"
+                        + " and use the 'values' restriction.",
+                    aspect.getName(), aspectAttrName);
+              }
+            }
+
+            // Required aspect parameters must be specified by the rule propagating the aspect with
+            // the same parameter type.
+            if (requiredAspectParameters.contains(aspectAttrName)) {
+              if (!ruleClass.hasAttr(aspectAttrName, aspectAttrType)) {
+                throw Starlark.errorf(
+                    "Aspect %s requires rule %s to specify attribute '%s' with type %s.",
+                    aspect.getName(), ruleClass.getName(), aspectAttrName, aspectAttrType);
+              }
+            }
+          }
+        }
+      }
+    }
+
     /** Export a RuleFunction from a Starlark file with a given name. */
     // To avoid losing event information in the case where the rule was defined with an explicit
     // name= arg, all events should be created using errorf(). See the comment in rule() above for
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AspectsListBuilder.java b/src/main/java/com/google/devtools/build/lib/packages/AspectsListBuilder.java
index 2863aab..04f7f31 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/AspectsListBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/AspectsListBuilder.java
@@ -112,7 +112,7 @@
 
   /** Wraps the information necessary to construct an Aspect. */
   @VisibleForSerialization
-  abstract static class AspectDetails<C extends AspectClass> {
+  public abstract static class AspectDetails<C extends AspectClass> {
     final C aspectClass;
     final Function<Rule, AspectParameters> parametersExtractor;
     final String baseAspectName;
@@ -132,14 +132,18 @@
       this.baseAspectName = baseAspectName;
     }
 
-    String getName() {
+    public String getName() {
       return this.aspectClass.getName();
     }
 
-    ImmutableSet<String> getRequiredParameters() {
+    public ImmutableSet<String> getRequiredParameters() {
       return ImmutableSet.of();
     }
 
+    public ImmutableList<Attribute> getAspectAttributes() {
+      return ImmutableList.of();
+    }
+
     protected abstract Aspect getAspect(Rule rule);
 
     protected abstract Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters)
@@ -194,6 +198,11 @@
     }
 
     @Override
+    public ImmutableList<Attribute> getAspectAttributes() {
+      return aspect.getAttributes();
+    }
+
+    @Override
     public Aspect getAspect(Rule rule) {
       AspectParameters params = parametersExtractor.apply(rule);
       return Aspect.forStarlark(aspectClass, aspect.getDefinition(params), params);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
index 558ab43..c4a8388 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
@@ -227,14 +227,6 @@
     }
   }
 
-  public ImmutableMap<String, ImmutableSet<String>> getRequiredAspectParameters() {
-    ImmutableMap.Builder<String, ImmutableSet<String>> paramBuilder = ImmutableMap.builder();
-    for (AspectDetails<?> aspect : aspects) {
-      paramBuilder.put(aspect.getName(), aspect.getRequiredParameters());
-    }
-    return paramBuilder.build();
-  }
-
   /**
    * Creates a new attribute builder.
    *
@@ -1599,7 +1591,7 @@
       super(useHostConfiguration, fragmentClass, defaultValue);
     }
   }
-  
+
   @AutoCodec.VisibleForSerialization
   static class AlwaysNullLateBoundDefault extends SimpleLateBoundDefault<Void, Void> {
     @SerializationConstant @AutoCodec.VisibleForSerialization
@@ -2098,6 +2090,10 @@
     return result.build();
   }
 
+  public ImmutableList<AspectDetails<?>> getAspectsDetails() {
+    return aspects;
+  }
+
   /**
    * Returns the default value of this attribute in the context of the specified Rule. For
    * attributes with a computed default, i.e. {@code hasComputedDefault()}, {@code rule} must be
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkDefinedAspect.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkDefinedAspect.java
index 279a1c6..df2d628 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkDefinedAspect.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkDefinedAspect.java
@@ -148,7 +148,21 @@
             getName(),
             attr.getName(),
             aspectParams.getAttribute(attr.getName()).size());
-        attr = attr.cloneBuilder(Type.STRING).value(value).build(attr.getName());
+
+        if (!attr.checkAllowedValues()) {
+          // The aspect attribute can have no allowed values constraint if the aspect is used from
+          // command-line. However, AspectDefinition.Builder$add requires the existance of allowed
+          // values in all aspects string attributes for both native and starlark aspects.
+          // Therefore, allowedValues list is added here with only the current value of the
+          // attribute.
+          attr =
+              attr.cloneBuilder(Type.STRING)
+                  .value(value)
+                  .allowedValues(new Attribute.AllowedValueSet(value))
+                  .build(attr.getName());
+        } else {
+          attr = attr.cloneBuilder(Type.STRING).value(value).build(attr.getName());
+        }
       }
       builder.add(attr);
     }
@@ -240,14 +254,15 @@
           parametersValues.getOrDefault(
               parameterName, (String) aspectParameter.getDefaultValue(null));
 
-      PredicateWithMessage<Object> allowedValues = aspectParameter.getAllowedValues();
-      if (allowedValues.apply(parameterValue)) {
-        builder.addAttribute(parameterName, parameterValue);
-      } else {
-        throw Starlark.errorf(
-            "%s: invalid value in '%s' attribute: %s",
-            getName(), parameterName, allowedValues.getErrorReason(parameterValue));
+      if (aspectParameter.checkAllowedValues()) {
+        PredicateWithMessage<Object> allowedValues = aspectParameter.getAllowedValues();
+        if (!allowedValues.apply(parameterValue)) {
+          throw Starlark.errorf(
+              "%s: invalid value in '%s' attribute: %s",
+              getName(), parameterName, allowedValues.getErrorReason(parameterValue));
+        }
       }
+      builder.addAttribute(parameterName, parameterValue);
     }
     return builder.build();
   }
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
index ef91b34b..086077e 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
@@ -7216,6 +7216,128 @@
             + " 'p_v2' instead of ''");
   }
 
+  @Test
+  public void testTopLevelAspectsWithParameters_noNeedForAllowedValues() throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        "def _aspect_a_impl(target, ctx):",
+        "  result = ['aspect_a on target {}, p = {}'.",
+        "                                    format(target.label, ctx.attr.p)]",
+        "  if ctx.rule.attr.dep:",
+        "    result += ctx.rule.attr.dep.aspect_a_result",
+        "  return struct(aspect_a_result = result)",
+        "",
+        "aspect_a = aspect(",
+        "  implementation = _aspect_a_impl,",
+        "  attr_aspects = ['dep'],",
+        "  attrs = { 'p' : attr.string(default='val') },",
+        ")",
+        "",
+        "def _my_rule_impl(ctx):",
+        "  pass",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "   attrs = { 'dep' : attr.label() },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:defs.bzl', 'my_rule')",
+        "my_rule(",
+        "  name = 'main_target',",
+        "  dep = ':dep_target_1',",
+        ")",
+        "my_rule(",
+        "  name = 'dep_target_1',",
+        "  dep = ':dep_target_2',",
+        ")",
+        "my_rule(",
+        "  name = 'dep_target_2',",
+        ")");
+    useConfiguration("--experimental_allow_top_level_aspects_parameters");
+
+    AnalysisResult analysisResult =
+        update(
+            ImmutableList.of("//test:defs.bzl%aspect_a"),
+            ImmutableMap.of("p", "p_v"),
+            "//test:main_target");
+
+    ImmutableMap<AspectKey, ConfiguredAspect> configuredAspects = analysisResult.getAspectsMap();
+    ConfiguredAspect aspectA = getConfiguredAspect(configuredAspects, "aspect_a");
+    assertThat(aspectA).isNotNull();
+    StarlarkList<?> aspectAResult = (StarlarkList) aspectA.get("aspect_a_result");
+    assertThat(Starlark.toIterable(aspectAResult))
+        .containsExactly(
+            "aspect_a on target //test:main_target, p = p_v",
+            "aspect_a on target //test:dep_target_1, p = p_v",
+            "aspect_a on target //test:dep_target_2, p = p_v");
+  }
+
+  /**
+   * Aspect parameter has to require set of values only if the aspect is used in a rule attribute.
+   */
+  @Test
+  public void testAttrAspectParameterMissingRequiredValues() throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.string(default = 'c') }",
+        ")",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "r1 = rule(_rule_impl, attrs={'dep': attr.label(aspects = [my_aspect])})");
+    scratch.file("test/BUILD", "load('//test:defs.bzl', 'r1')", "r1(name = 'main_target')");
+    reporter.removeHandler(failFastHandler);
+
+    // This call succeeds if "--keep_going" was passed, which it does in the WithKeepGoing test
+    // suite. Otherwise, it fails and throws a TargetParsingException.
+    if (keepGoing()) {
+      AnalysisResult analysisResult = update("//test:main_target");
+      assertThat(analysisResult.hasError()).isTrue();
+    } else {
+      assertThrows(TargetParsingException.class, () -> update("//test:main_target"));
+    }
+    assertContainsEvent(
+        "Aspect //test:defs.bzl%my_aspect: Aspect parameter attribute 'param' must have type"
+            + " 'string' and use the 'values' restriction.");
+  }
+
+  /**
+   * Aspect parameter has to require set of values only if the aspect is used in a rule attribute.
+   */
+  @Test
+  public void testAttrRequiredAspectParameterMissingRequiredValues() throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        "def _impl(target, ctx):",
+        "   pass",
+        "required_aspect = aspect(_impl,",
+        "   attrs = { 'p1' : attr.string(default = 'b') }",
+        ")",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'p2' : attr.string(default = 'c', values = ['c']) },",
+        "   requires = [required_aspect],",
+        ")",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "r1 = rule(_rule_impl, attrs={'dep': attr.label(aspects = [my_aspect])})");
+    scratch.file("test/BUILD", "load('//test:defs.bzl', 'r1')", "r1(name = 'main_target')");
+    reporter.removeHandler(failFastHandler);
+
+    // This call succeeds if "--keep_going" was passed, which it does in the WithKeepGoing test
+    // suite. Otherwise, it fails and throws a TargetParsingException.
+    if (keepGoing()) {
+      AnalysisResult analysisResult = update("//test:main_target");
+      assertThat(analysisResult.hasError()).isTrue();
+    } else {
+      assertThrows(TargetParsingException.class, () -> update("//test:main_target"));
+    }
+    assertContainsEvent(
+        "Aspect //test:defs.bzl%required_aspect: Aspect parameter attribute 'p1' must have type"
+            + " 'string' and use the 'values' restriction.");
+  }
+
   private ConfiguredAspect getConfiguredAspect(
       Map<AspectKey, ConfiguredAspect> aspectsMap, String aspectName) {
     for (Map.Entry<AspectKey, ConfiguredAspect> entry : aspectsMap.entrySet()) {
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java
index b2f580f..16edc87 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java
@@ -571,22 +571,51 @@
   }
 
   @Test
-  public void testAspectParameterRequiresValues() throws Exception {
-    ev.checkEvalErrorContains(
-        "Aspect parameter attribute 'param' must have type 'string' and use the 'values' "
-            + "restriction.",
+  public void testAspectParameterWithDefaultValue() throws Exception {
+    evalAndExport(
+        ev,
         "def _impl(target, ctx):",
         "   pass",
         "my_aspect = aspect(_impl,",
-        "   attrs = { 'param' : attr.string(default = 'c') }",
+        "   attrs = { 'param' : attr.string(default = 'a', values=['a', 'b']) }",
         ")");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    Attribute attribute = Iterables.getOnlyElement(aspect.getAttributes());
+    assertThat(attribute.getName()).isEqualTo("param");
+    assertThat(((String) attribute.getDefaultValueUnchecked())).isEqualTo("a");
+  }
+
+  @Test
+  public void testAspectParameterBadDefaultValue() throws Exception {
+    ev.checkEvalErrorContains(
+        "Aspect parameter attribute 'param' has a bad default value: has to be"
+            + " one of 'b' instead of 'a'",
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.string(default = 'a', values = ['b']) }",
+        ")");
+  }
+
+  @Test
+  public void testAspectParameterNotRequireValues() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.string(default = 'val') }",
+        ")");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    Attribute attribute = Iterables.getOnlyElement(aspect.getAttributes());
+    assertThat(attribute.getName()).isEqualTo("param");
+    assertThat(((String) attribute.getDefaultValueUnchecked())).isEqualTo("val");
   }
 
   @Test
   public void testAspectParameterBadType() throws Exception {
     ev.checkEvalErrorContains(
-        "Aspect parameter attribute 'param' must have type 'string' and use the 'values' "
-            + "restriction.",
+        "Aspect parameter attribute 'param' must have type 'string'.",
         "def _impl(target, ctx):",
         "   pass",
         "my_aspect = aspect(_impl,",