// Copyright 2023 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.build.lib.analysis.starlark;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.analysis.starlark.StarlarkSubrule.getRuleAttrName;
import static org.junit.Assert.assertThrows;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.config.transitions.StarlarkExposedRuleTransitionFactory;
import com.google.devtools.build.lib.analysis.platform.ToolchainInfo;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.AttributeValueSource;
import com.google.devtools.build.lib.packages.Provider;
import com.google.devtools.build.lib.packages.StarlarkInfo;
import com.google.devtools.build.lib.packages.StarlarkProvider;
import com.google.devtools.build.lib.packages.StructImpl;
import com.google.devtools.build.lib.rules.java.JavaToolchainProvider;
import com.google.devtools.build.lib.starlark.util.BazelEvaluationTestCase;
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkSubruleApi;
import com.google.devtools.build.lib.starlarkbuildapi.cpp.CppConfigurationApi;
import com.google.devtools.build.lib.testutil.TestConstants;
import net.starlark.java.eval.BuiltinFunction;
import net.starlark.java.eval.Sequence;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class StarlarkSubruleTest extends BuildViewTestCase {

  private final BazelEvaluationTestCase ev = new BazelEvaluationTestCase("//subrule_testing:label");
  private final BazelEvaluationTestCase evOutsideAllowlist =
      new BazelEvaluationTestCase("//foo:bar");

  @Test
  public void testSubruleFunctionSymbol_notVisibleInBUILD() throws Exception {
    scratch.file("foo/BUILD", "subrule");

    checkLoadingPhaseError("//foo", "'subrule' is not defined");
  }

  @Test
  // checks that 'subrule' symbol visibility in bzl files, not whether it's callable
  public void testSubruleFunctionSymbol_isVisibleInBzl() throws Exception {
    Object subruleFunction = ev.eval("subrule");

    assertNoEvents();
    assertThat(subruleFunction).isNotNull();
    assertThat(subruleFunction).isInstanceOf(BuiltinFunction.class);
  }

  @Test
  public void testSubruleInstantiation_inAllowlistedPackage_succeeds() throws Exception {
    Object subrule = ev.eval("subrule(implementation = lambda : 0 )");

    assertThat(subrule).isNotNull();
    assertThat(subrule).isInstanceOf(StarlarkSubruleApi.class);
  }

  @Test
  public void testSubrule_isCallableOnlyFromRuleOrAspectImplementation() throws Exception {
    ev.execAndExport("x = subrule(implementation = lambda : 'dummy result')");

    ev.checkEvalErrorContains("x can only be called from a rule or aspect implementation", "x()");
  }

  @Test
  public void testSubrule_isCallableOnlyAfterExport() throws Exception {
    ev.checkEvalErrorContains(
        "Invalid subrule hasn't been exported by a bzl file",
        "unexported = [subrule(implementation = lambda: None)]",
        "unexported[0]()");
  }

  @Test
  public void testSubrule_ruleMustDeclareSubrule() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "_my_subrule = subrule(implementation = lambda : '')",
        "",
        "def _rule_impl(ctx):",
        "  _my_subrule()",
        "",
        "my_rule = rule(implementation = _rule_impl)");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error in _my_subrule: rule 'my_rule' must declare '_my_subrule' in" + " 'subrules'");
  }

  @Test
  public void testSubrule_childAndParentSubrules() throws Exception {
    scratch.file(
        "subrule_testing/parent.bzl",
        "_parent_subrule = subrule(implementation = lambda ctx: None)",
        "def _rule_impl(ctx):",
        "  _parent_subrule()",
        "parent_rule = rule(implementation = _rule_impl, extendable = True, subrules ="
            + " [_parent_subrule])");
    scratch.file(
        "subrule_testing/child.bzl",
        "load('parent.bzl', 'parent_rule')",
        "_child_subrule = subrule(implementation = lambda ctx: None)",
        "def _rule_impl(ctx):",
        "  ctx.super()",
        "  _child_subrule()",
        "child_rule = rule(implementation = _rule_impl, parent = parent_rule, subrules ="
            + " [_child_subrule])");
    scratch.file(
        "subrule_testing/BUILD", "load('child.bzl', 'child_rule')", "child_rule(name = 'foo')");

    getConfiguredTarget("//subrule_testing:foo");

    assertNoEvents();
  }

  @Test
  public void testSubrule_childAndParent_canUseTheSameSubrule() throws Exception {
    scratch.file(
        "subrule_testing/parent.bzl",
        "parent_subrule = subrule(",
        "  implementation = lambda ctx, _tool: None,",
        "  attrs = {'_tool': attr.label(default = ':tool')}",
        ")",
        "def _rule_impl(ctx):",
        "  parent_subrule()",
        "parent_rule = rule(implementation = _rule_impl, extendable = True, subrules ="
            + " [parent_subrule])");
    scratch.file(
        "subrule_testing/child.bzl",
        "load('parent.bzl', 'parent_rule', 'parent_subrule')",
        "def _rule_impl(ctx):",
        "  ctx.super()",
        "  parent_subrule()",
        "child_rule = rule(implementation = _rule_impl, parent = parent_rule, subrules ="
            + " [parent_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        "load('child.bzl', 'child_rule')",
        "child_rule(name = 'foo')",
        "filegroup(name = 'tool')");

    getConfiguredTarget("//subrule_testing:foo");

    assertNoEvents();
  }

  @Test
  public void testSubrule_childCantUseParentsSubrule() throws Exception {
    scratch.file(
        "subrule_testing/parent.bzl",
        "parent_subrule = subrule(implementation = lambda ctx: None)",
        "def _rule_impl(ctx):",
        "  parent_subrule()",
        "parent_rule = rule(implementation = _rule_impl, extendable = True, subrules ="
            + " [parent_subrule])");
    scratch.file(
        "subrule_testing/child.bzl",
        "load('parent.bzl', 'parent_rule', 'parent_subrule')",
        "_child_subrule = subrule(implementation = lambda ctx: None)",
        "def _rule_impl(ctx):",
        "  ctx.super()",
        "  parent_subrule()",
        "child_rule = rule(implementation = _rule_impl, parent = parent_rule)");
    scratch.file(
        "subrule_testing/BUILD", "load('child.bzl', 'child_rule')", "child_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error in parent_subrule: rule 'child_rule' must declare 'parent_subrule' in"
                + " 'subrules'");
  }

  @Test
  public void testSubrule_parentCantUseChildsSubrule() throws Exception {
    scratch.file(
        "subrule_testing/parent.bzl",
        "my_subrule = subrule(implementation = lambda ctx: None)",
        "def _rule_impl(ctx):",
        "  my_subrule()",
        "parent_rule = rule(implementation = _rule_impl, extendable = True)");
    scratch.file(
        "subrule_testing/child.bzl",
        "load('parent.bzl', 'parent_rule', 'my_subrule')",
        "def _rule_impl(ctx):",
        "  ctx.super()",
        "  my_subrule()",
        "child_rule = rule(implementation = _rule_impl, parent = parent_rule, subrules ="
            + " [my_subrule])");
    scratch.file(
        "subrule_testing/BUILD", "load('child.bzl', 'child_rule')", "child_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error in my_subrule: rule 'parent_rule' must declare 'my_subrule' in 'subrules'");
  }

  @Test
  public void testSubrule_aspectMustDeclareSubrule() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "_my_subrule = subrule(implementation = lambda ctx: 'dummy aspect result')",
        "",
        "def _aspect_impl(ctx,target):",
        "  res = _my_subrule()",
        "",
        "_my_aspect = aspect(implementation = _aspect_impl)",
        "",
        "my_rule = rule(",
        "  implementation = lambda ctx: [],",
        "  attrs = {'dep' : attr.label(mandatory = True, aspects = [_my_aspect])},",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "java_library(name = 'bar')",
        "my_rule(name = 'foo', dep = 'bar')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error in _my_subrule: aspect '//subrule_testing:myrule.bzl%_my_aspect' must"
                + " declare '_my_subrule' in 'subrules'");
  }

  @Test
  public void testSubruleCallingUndeclaredSibling_fails() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule1_impl(ctx):",
        "  return 'result from subrule1'",
        "_my_subrule1 = subrule(implementation = _subrule1_impl)",
        "",
        "def _subrule2_impl(ctx):",
        "  return _my_subrule1()",
        "_my_subrule2 = subrule(implementation = _subrule2_impl)",
        "",
        "MyInfo=provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule2()",
        "  return [MyInfo(result = res)]",
        "",
        "my_rule = rule(_rule_impl, subrules = [_my_subrule2, _my_subrule1])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error).isNotNull();
    assertThat(error)
        .hasMessageThat()
        .contains("subrule _my_subrule2 must declare _my_subrule1 in 'subrules'");
  }

  @Test
  public void testSubrule_implementationMustAcceptSubruleContext() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "_my_subrule = subrule(implementation = lambda : '')",
        "",
        "def _rule_impl(ctx):",
        "  _my_subrule()",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains("Error: lambda() does not accept positional arguments, but got 1");
  }

  @Test
  public void testSubrule_isCallableFromRule() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "_my_subrule = subrule(implementation = lambda ctx: 'dummy rule result')",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    StructImpl provider =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo");

    assertThat(provider).isNotNull();
    assertThat(provider.getValue("result")).isEqualTo("dummy rule result");
  }

  @Test
  public void testSubrule_isCallableFromAspect() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "_my_subrule = subrule(implementation = lambda ctx: 'dummy aspect result')",
        "",
        "MyInfo = provider()",
        "def _aspect_impl(ctx,target):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "_my_aspect = aspect(implementation = _aspect_impl, subrules = [_my_subrule])",
        "",
        "my_rule = rule(",
        "  implementation = lambda ctx: [ctx.attr.dep[MyInfo]],",
        "  attrs = {'dep' : attr.label(mandatory = True, aspects = [_my_aspect])},",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "java_library(name = 'bar')",
        "my_rule(name = 'foo', dep = 'bar')");

    StructImpl provider =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo");

    assertThat(provider).isNotNull();
    assertThat(provider.getValue("result")).isEqualTo("dummy aspect result");
  }

  @Test
  public void testSubrule_subruleContextExposesRuleLabel() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return 'called in: ' + str(ctx.label)",
        "_my_subrule = subrule(implementation = _subrule_impl)",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    StructImpl provider =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo");

    assertThat(provider).isNotNull();
    assertThat(provider.getValue("result")).isEqualTo("called in: @@//subrule_testing:foo");
  }

  @Test
  public void testSubrule_subruleContextExposesActionsApi() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  out = ctx.actions.declare_file(ctx.label.name + '.out')",
        "  ctx.actions.write(out, 'subrule file content')",
        "  return out",
        "_my_subrule = subrule(implementation = _subrule_impl)",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    Artifact artifact =
        (Artifact)
            getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
                .getValue("result");

    assertThat(artifact).isNotNull();
    assertThat(artifact.getFilename()).isEqualTo("foo.out");
    assertThat(((FileWriteAction) getGeneratingAction(artifact)).getFileContents())
        .isEqualTo("subrule file content");
  }

  @Test
  public void testSubruleActions_run_doesNotAllowSettingToolchain() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  out = ctx.actions.declare_file(ctx.label.name + '.out')",
        "  ctx.actions.run(toolchain = 'foo', executable = '/path/to/tool', outputs = [out])",
        "",
        "_my_subrule = subrule(implementation = _subrule_impl)",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  _my_subrule()",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error).hasMessageThat().contains("'toolchain' may not be specified in subrules");
  }

  @Test
  public void testSubruleActions_run_doesNotAllowSettingExecGroup() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  out = ctx.actions.declare_file(ctx.label.name + '.out')",
        "  ctx.actions.run(exec_group = 'foo', executable = '/path/to/tool', outputs = [out])",
        "",
        "_my_subrule = subrule(implementation = _subrule_impl)",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  _my_subrule()",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error).hasMessageThat().contains("'exec_group' may not be specified in subrules");
  }

  @Test
  public void testSubruleContext_cannotBeUsedOutsideImplementationFunction() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return ctx",
        "",
        "_my_subrule = subrule(implementation = _subrule_impl)",
        "",
        "def _rule_impl(ctx):",
        "  subrule_ctx = _my_subrule()",
        "  subrule_ctx.label",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error: cannot access field or method 'label' of subrule context outside of its own"
                + " implementation function");
  }

  @Test
  public void testRuleContext_cannotBeUsedInSubruleImplementation() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, rule_ctx):",
        "  rule_ctx.label",
        "",
        "_my_subrule = subrule(implementation = _subrule_impl)",
        "",
        "def _rule_impl(ctx):",
        "  subrule_ctx = _my_subrule(ctx)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error: cannot access field or method 'label' of rule context for"
                + " '//subrule_testing:foo' outside of its own rule implementation function");
  }

  @Test
  public void testSubruleAttrs_publicAttributesAreNotPermitted() throws Exception {
    ev.checkEvalErrorContains(
        "illegal attribute name 'foo': subrules may only define private attributes",
        "subrule(implementation = lambda: None, attrs = {'foo': attr.string()})");
  }

  @Test
  public void testSubruleAttrs_computedDefaultsAreNotPermitted() throws Exception {
    ev.checkEvalErrorContains(
        "for attribute '_foo': subrules cannot define computed defaults.",
        "subrule(",
        "  implementation = lambda: None,",
        "  attrs = {'_foo': attr.label(default = lambda: '')}",
        ")");
  }

  @Test
  public void testSubruleAttrs_onlyLabelsOrLabelListsPermitted() throws Exception {
    ev.checkEvalErrorContains(
        "bad type for attribute '_foo': subrule attributes may only be label or lists of labels.",
        "subrule(",
        "  implementation = lambda: None,",
        "  attrs = {'_foo': attr.int()}",
        ")");
  }

  @Test
  public void testSubruleAttrs_attributeMustHaveDefaultValue() throws Exception {
    ev.checkEvalErrorContains(
        "for attribute '_foo': no default value specified",
        "subrule(",
        "  implementation = lambda: None,",
        "  attrs = {'_foo': attr.label()}",
        ")");
  }

  @Test
  public void testSubruleAttrs_cannotHaveStarlarkTransitions() throws Exception {
    ev.checkEvalErrorContains(
        "bad cfg for attribute '_foo': subrules may only have target/exec attributes.",
        "my_transition = transition(implementation = lambda: None, inputs = [], outputs = [])",
        "_my_subrule = subrule(",
        "  implementation = lambda: None,",
        "  attrs = {'_foo': attr.label(cfg = my_transition)}",
        ")");
  }

  @Test
  public void testSubruleAttrs_cannotHaveNativeTransitions() throws Exception {
    ev.update("native_transition", (StarlarkExposedRuleTransitionFactory) data -> null);
    ev.checkEvalErrorContains(
        "bad cfg for attribute '_foo': subrules may only have target/exec attributes.",
        "_my_subrule = subrule(",
        "  implementation = lambda: None,",
        "  attrs = {'_foo': attr.label(cfg = native_transition)}",
        ")");
  }

  @Test
  public void testSubruleAttrs_notVisibleInRuleCtx() throws Exception {
    scratch.file("default/BUILD", "genrule(name = 'default', outs = ['a'], cmd = '')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return ",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_foo' : attr.label(default = '//default')},",
        ")",
        "MyInfo=provider()",
        "def _rule_impl(ctx):",
        "  res = dir(ctx.attr)",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    ImmutableList<String> ruleClassAttributes =
        getRuleContext(getConfiguredTarget("//subrule_testing:foo"))
            .getRule()
            .getRuleClassObject()
            .getAttributes()
            .stream()
            .map(Attribute::getName)
            .collect(toImmutableList());
    ImmutableList<String> attributesVisibleToStarlark =
        Sequence.cast(
                getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
                    .getValue("result"),
                String.class,
                "")
            .getImmutableList();
    String ruleAttrName =
        getRuleAttrName(
            Label.parseCanonical("//subrule_testing:myrule.bzl"),
            "_my_subrule",
            "_foo",
            AttributeValueSource.DIRECT);

    assertThat(ruleClassAttributes).contains(ruleAttrName);
    assertThat(attributesVisibleToStarlark).doesNotContain(ruleAttrName);
  }

  @Test
  public void testSubruleAttrs_notVisibleInAspectCtx() throws Exception {
    scratch.file("default/BUILD", "genrule(name = 'default', outs = ['a'], cmd = '')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "_my_subrule = subrule(",
        "  implementation = lambda: None,",
        "  attrs = {'_foo' : attr.label(default = '//default')},",
        ")",
        "MyInfo=provider()",
        "def _aspect_impl(target, ctx):",
        "  res = dir(ctx.attr)",
        "  return MyInfo(result = res)",
        "my_aspect = aspect(implementation = _aspect_impl, subrules = [_my_subrule])",
        "def _rule_impl(ctx):",
        "  return ctx.attr.dep[MyInfo]",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  attrs = {'dep' : attr.label(aspects = [my_aspect])}",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo', dep = '//default')");

    ImmutableList<String> attributesVisibleToStarlark =
        Sequence.cast(
                getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
                    .getValue("result"),
                String.class,
                "")
            .getImmutableList();
    String ruleAttrName =
        getRuleAttrName(
            Label.parseCanonical("//subrule_testing:myrule.bzl"),
            "_my_subrule",
            "_foo",
            AttributeValueSource.DIRECT);

    assertThat(attributesVisibleToStarlark).doesNotContain(ruleAttrName);
  }

  @Test
  public void testSubruleAttrs_overridingImplicitAttributeValueFails() throws Exception {
    scratch.file("default/BUILD", "genrule(name = 'default', outs = ['a'], cmd = '')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, _foo):",
        "  return ",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_foo' : attr.label(default = '//default')},",
        ")",
        "",
        "def _rule_impl(ctx):",
        "  res = _my_subrule(_foo = '//override')",
        "  return []",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "Error in _my_subrule: got invalid named argument: '_foo' is an implicit dependency and"
                + " cannot be overridden");
  }

  @Test
  public void testSubruleAttrs_implicitLabelDepsAreResolvedToTargets() throws Exception {
    scratch.file(
        "some/pkg/BUILD",
        //
        "genrule(name = 'tool', cmd = '', outs = ['tool.exe'])");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, _tool):",
        "  return _tool",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_tool' : attr.label(default = '//some/pkg:tool')},",
        ")",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    StructImpl provider =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo");

    assertThat(provider).isNotNull();
    Object value = provider.getValue("result");
    assertThat(value).isInstanceOf(ConfiguredTarget.class);
    assertThat(((ConfiguredTarget) value).getLabel().toString()).isEqualTo("//some/pkg:tool");
  }

  @Test
  public void testSubruleAttrs_singleFileLabelAttributesAreResolvedToFile() throws Exception {
    scratch.file(
        "some/pkg/BUILD",
        //
        "genrule(name = 'tool', cmd = '', outs = ['tool.exe'])");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, _tool):",
        "  return _tool",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_tool' : attr.label(allow_single_file = True, default = '//some/pkg:tool')},",
        ")",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    StructImpl provider =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo");

    assertThat(provider).isNotNull();
    Object value = provider.getValue("result");
    assertThat(value).isInstanceOf(Artifact.class);
    assertThat(((Artifact) value).getRootRelativePathString()).isEqualTo("some/pkg/tool.exe");
  }

  @Test
  public void testSubruleAttr_executableAttrIsPassedAsFilesToRun() throws Exception {
    scratch.file(
        "my/BUILD",
        //
        "cc_binary(name = 'tool')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, _tool):",
        "  return _tool",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_tool' : attr.label(default = '//my:tool', executable = True, cfg = 'exec')},",
        ")",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    Object result =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result");

    assertThat(result).isInstanceOf(FilesToRunProvider.class);
    assertThat(((FilesToRunProvider) result).getExecutable().getRootRelativePathString())
        .isEqualTo("my/tool");
  }

  @Test
  public void testSubruleAction_executableMustBeFilesToRunProvider() throws Exception {
    scratch.file(
        "my/BUILD",
        //
        "cc_binary(name = 'tool')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, _tool):",
        "  out = ctx.actions.declare_file(ctx.label.name + '.out')",
        "  ctx.actions.run(executable = _tool.executable, outputs = [out])",
        "  return out",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_tool' : attr.label(default = '//my:tool', executable = True, cfg = 'exec')},",
        ")",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains("Error in run: for 'executable', expected FilesToRunProvider, got File");
  }

  @Test
  public void testSubruleAttrs_lateBoundDefaultsAreResolved() throws Exception {
    scratch.file(
        "my/BUILD",
        //
        "cc_binary(name = 'tool')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx, _tool):",
        "  return _tool",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  attrs = {'_tool' : attr.label(",
        "         default = configuration_field(fragment = 'coverage', name = 'output_generator')",
        "  )},",
        ")",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(implementation = _rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");
    // TODO: b/293304174 - use a custom fragment instead of coverage
    useConfiguration("--collect_code_coverage", "--coverage_output_generator=//my:tool");

    StructImpl provider =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo");

    assertThat(provider).isNotNull();
    Object value = provider.getValue("result");
    assertThat(value).isInstanceOf(ConfiguredTarget.class);
    assertThat(((ConfiguredTarget) value).getLabel().toString()).isEqualTo("//my:tool");
  }

  @Test
  public void testSubruleToolchains_cannotRequireMoreThanOne() throws Exception {
    ev.checkEvalErrorContains(
        "subrules may require at most 1 toolchain",
        "_my_subrule = subrule(",
        "  implementation = lambda: None,",
        "  toolchains = ['//t1', '//t2'],",
        ")");
  }

  @Test
  public void testSubruleToolchains_cannotAccessUnrequestedToolchain() throws Exception {
    useConfiguration("--incompatible_auto_exec_groups");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  ctx.toolchains['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "']",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        ")",
        "",
        "def _rule_impl(ctx):",
        "  _my_subrule()",
        "",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  subrules = [_my_subrule],",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    assertThrows(
        TestConstants.JAVA_TOOLCHAIN_TYPE + " was requested but only types [] are configured",
        AssertionError.class,
        () -> getConfiguredTarget("//subrule_testing:foo"));
  }

  @Test
  public void testSubruleToolchains_cannotAccessToolchainFromRule() throws Exception {
    useConfiguration("--incompatible_auto_exec_groups");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  ctx.toolchains['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "']",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        ")",
        "",
        "def _rule_impl(ctx):",
        "  _my_subrule()",
        "",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  subrules = [_my_subrule],",
        "  toolchains = ['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "'],",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    assertThrows(
        TestConstants.JAVA_TOOLCHAIN_TYPE + " was requested but only types [] are configured",
        AssertionError.class,
        () -> getConfiguredTarget("//subrule_testing:foo"));
  }

  @Test
  public void testSubruleToolchains_requestedToolchainIsResolved_inRule() throws Exception {
    useConfiguration("--incompatible_auto_exec_groups");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return ctx.toolchains['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "']",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  toolchains = ['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "'],",
        ")",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  return [MyInfo(result = _my_subrule())]",
        "",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  subrules = [_my_subrule],",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    ToolchainInfo toolchainInfo =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result", ToolchainInfo.class);

    assertThat(toolchainInfo).isNotNull();
    assertThat(toolchainInfo.getValue("java", StarlarkInfo.class).getProvider().getKey())
        .isEqualTo(JavaToolchainProvider.PROVIDER.getKey());
  }

  @Test
  public void testSubruleToolchains_requstedToolchainIsResolved_inAspect() throws Exception {
    useConfiguration("--incompatible_auto_exec_groups");
    scratch.file("default/BUILD", "genrule(name = 'default', outs = ['a'], cmd = '')");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return ctx.toolchains['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "']",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  toolchains = ['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "'],",
        ")",
        "MyInfo=provider()",
        "def _aspect_impl(target, ctx):",
        "  return MyInfo(result = _my_subrule())",
        "my_aspect = aspect(implementation = _aspect_impl, subrules = [_my_subrule])",
        "def _rule_impl(ctx):",
        "  return ctx.attr.dep[MyInfo]",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  attrs = {'dep' : attr.label(aspects = [my_aspect])}",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo', dep = '//default')");

    ToolchainInfo toolchainInfo =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result", ToolchainInfo.class);

    assertThat(toolchainInfo).isNotNull();
    assertThat(toolchainInfo.getValue("java", StarlarkInfo.class).getProvider().getKey())
        .isEqualTo(JavaToolchainProvider.PROVIDER.getKey());
  }

  /**
   * This test case exists purely as a companion to the test case {@link
   * #testSubruleToolchains_requestedToolchainIsSuppliedToAction} below. To ensure the latter does
   * not spuriously pass, we test the inverse case here. If this test begins to fail due to changes
   * to the AEG checking code, it very likely means the other test below needs to be updated
   * appropriately.
   */
  @Test
  public void testSubruleToolchains_aegValidationFailsWithoutToolchain() throws Exception {
    useConfiguration("--incompatible_auto_exec_groups");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  out = ctx.actions.declare_file(ctx.label.name + '.out')",
        "  ctx.actions.run(outputs = [out], executable = '/bin/ls', tools = [depset()])",
        "  return out",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        ")",
        "def _rule_impl(ctx):",
        "  return [DefaultInfo(files = depset([_my_subrule()]))]",
        "",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  subrules = [_my_subrule],",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    assertThrows(
        "Couldn't identify if tools are from implicit dependencies or a toolchain. Please set the"
            + " toolchain parameter.",
        AssertionError.class,
        () -> getConfiguredTarget("//subrule_testing:foo"));
  }

  @Test
  public void testSubruleToolchains_requestedToolchainIsSuppliedToAction() throws Exception {
    useConfiguration("--incompatible_auto_exec_groups");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  out = ctx.actions.declare_file(ctx.label.name + '.out')",
        "  ctx.actions.run(outputs = [out], executable = '/bin/ls', tools = [depset()])",
        "  return out",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  toolchains = ['" + TestConstants.JAVA_TOOLCHAIN_TYPE + "'],",
        ")",
        "def _rule_impl(ctx):",
        "  return [DefaultInfo(files = depset([_my_subrule()]))]",
        "",
        "my_rule = rule(",
        "  implementation = _rule_impl,",
        "  subrules = [_my_subrule],",
        ")");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    ConfiguredTarget target = getConfiguredTarget("//subrule_testing:foo");
    Action action = getGeneratingAction(target, "subrule_testing/foo.out");

    assertThat(action).isNotNull();
    assertThat(action.getOwner())
        .isEqualTo(getRuleContext(target).getActionOwner(TestConstants.JAVA_TOOLCHAIN_TYPE));
  }

  @Test
  public void testSubruleFragments_errorForInvalidFragments() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return ctx.fragments.foobar",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl, fragments = ['java', 'cpp']",
        ")",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError assertionError =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(assertionError)
        .hasMessageThat()
        .contains(
            "There is no configuration fragment named 'foobar'. Available fragments: 'java',"
                + " 'cpp'");
  }

  @Test
  public void testSubruleFragments_onlyDeclaredFragmentsAreVisible() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return dir(ctx.fragments)",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  fragments = ['cpp', 'python']",
        ")",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_my_subrule], fragments = ['java'])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    Sequence<String> fragments =
        Sequence.cast(
            getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
                .getValue("result"),
            String.class,
            "ctx.fragments");

    assertThat(fragments).isNotNull();
    assertThat(fragments).containsExactly("cpp", "python");
  }

  @Test
  public void testSubruleFragments_ruleCannotAccessSubruleFragments() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  pass",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  fragments = ['cpp']",
        ")",
        "def _rule_impl(ctx):",
        "  ctx.fragments.cpp",
        "  return []",
        "",
        "my_rule = rule(_rule_impl, subrules = [_my_subrule])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError assertionError =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(assertionError)
        .hasMessageThat()
        .contains("my_rule has to declare 'cpp' as a required fragment in order to access it");
  }

  @Test
  public void testSubruleFragments_canAccessDeclaredFragments() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  return ctx.fragments.cpp",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  fragments = ['cpp']",
        ")",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_my_subrule], fragments = ['java'])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    CppConfigurationApi<?> fragment =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result", CppConfigurationApi.class);

    assertThat(fragment).isNotNull();
  }

  @Test
  public void testSubruleFragments_mustDeclareFragmentsIfAccessed() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _subrule_impl(ctx):",
        "  ctx.fragments.java",
        "_my_subrule = subrule(",
        "  implementation = _subrule_impl,",
        "  fragments = ['cpp', 'python']",
        ")",
        "",
        "def _rule_impl(ctx):",
        "  res = _my_subrule()",
        "  return []",
        "",
        "my_rule = rule(_rule_impl, subrules = [_my_subrule], fragments = ['java'])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError assertionError =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(assertionError)
        .hasMessageThat()
        .contains("_my_subrule has to declare 'java' as a required fragment in order to access it");
  }

  @Test
  public void testTransitiveSubrules_subruleMustDeclareCalledSubrule() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx):",
        "  return 'from subruleA'",
        "_A = subrule(implementation = _A_impl)",
        "",
        "def _B_impl(ctx):",
        "  return _A()",
        "_B = subrule(implementation = _B_impl)",
        "",
        "def _rule_impl(ctx):",
        "  res = _B()",
        "",
        "my_rule = rule(_rule_impl, subrules = [_B])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError assertionError =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(assertionError)
        .hasMessageThat()
        .contains("Error in _A: subrule _B must declare _A in 'subrules'");
  }

  @Test
  public void testTransitiveSubrules_ruleCannotCallUndeclaredTransitiveSubrule() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx):",
        "  return 'from subruleA'",
        "_A = subrule(implementation = _A_impl)",
        "",
        "def _B_impl(ctx):",
        "  return _A()",
        "_B = subrule(implementation = _B_impl, subrules = [_A])",
        "",
        "def _rule_impl(ctx):",
        "  _A()",
        "",
        "my_rule = rule(_rule_impl, subrules = [_B])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError assertionError =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(assertionError)
        .hasMessageThat()
        .contains("rule 'my_rule' must declare '_A' in 'subrules'");
  }

  @Test
  public void testTransitiveSubrules_subruleCanCallDeclaredSubrule() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx):",
        "  return 'from subruleA'",
        "_A = subrule(implementation = _A_impl)",
        "",
        "def _B_impl(ctx):",
        "  return _A()",
        "_B = subrule(implementation = _B_impl, subrules = [_A])",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _B()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_B])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    String result =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result", String.class);

    assertThat(result).isEqualTo("from subruleA");
  }

  @Test
  public void testTransitiveSubrules_arbitrarilyLongTransitiveChainsAreResolved() throws Exception {
    scratch.file("a/BUILD", "genrule(name = 'tool', cmd = '', outs = ['tool.out'])");
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx, _tool):",
        "  return 'tool name: ' + _tool.label.name",
        "_A = subrule(implementation=_A_impl, attrs = {'_tool':attr.label(default = '//a:tool')})",
        "_B = subrule(implementation=lambda ctx: _A(), subrules = [_A])",
        "_C = subrule(implementation=lambda ctx: _B(), subrules = [_B])",
        "_D = subrule(implementation=lambda ctx: _C(), subrules = [_C])",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  return MyInfo(result = _D())",
        "",
        "my_rule = rule(_rule_impl, subrules = [_D])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    String result =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result", String.class);

    assertThat(result).isEqualTo("tool name: tool");
  }

  @Test
  public void testTransitiveSubrules_ruleAndSubruleCanHaveCommonSubruleDependency()
      throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx):",
        "  return 'from subruleA'",
        "_A = subrule(implementation = _A_impl)",
        "",
        "def _B_impl(ctx):",
        "  _A()",
        "  return 'from subruleB'",
        "_B = subrule(implementation = _B_impl, subrules = [_A])",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  resA = _A()",
        "  resB = _B()",
        "  return MyInfo(resA = resA, resB = resB)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_A, _B])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    String resA =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("resA", String.class);
    String resB =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("resB", String.class);

    assertThat(resA).isEqualTo("from subruleA");
    assertThat(resB).isEqualTo("from subruleB");
  }

  @Test
  public void testTransitiveSubrules_callerSubruleCtxIsLocked() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx, ctxB):",
        "  return ctxB.label",
        "_A = subrule(implementation = _A_impl)",
        "",
        "def _B_impl(ctx):",
        "  return _A(ctx)",
        "_B = subrule(implementation = _B_impl, subrules = [_A])",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _B()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_B])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    AssertionError error =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//subrule_testing:foo"));

    assertThat(error)
        .hasMessageThat()
        .contains(
            "cannot access field or method 'label' of subrule context outside of its own"
                + " implementation function");
  }

  @Test
  public void testTransitiveSubrules_callerSubruleCtxIsUnlockedUponResumption() throws Exception {
    scratch.file(
        "subrule_testing/myrule.bzl",
        "def _A_impl(ctx):",
        "  return 'from A'",
        "_A = subrule(implementation = _A_impl)",
        "",
        "def _B_impl(ctx):",
        "  _A()",
        "  return 'from B: ' + ctx.label.name",
        "_B = subrule(implementation = _B_impl, subrules = [_A])",
        "",
        "MyInfo = provider()",
        "def _rule_impl(ctx):",
        "  res = _B()",
        "  return MyInfo(result = res)",
        "",
        "my_rule = rule(_rule_impl, subrules = [_B])");
    scratch.file(
        "subrule_testing/BUILD",
        //
        "load('myrule.bzl', 'my_rule')",
        "my_rule(name = 'foo')");

    String result =
        getProvider("//subrule_testing:foo", "//subrule_testing:myrule.bzl", "MyInfo")
            .getValue("result", String.class);

    assertThat(result).isEqualTo("from B: foo");
  }

  @Test
  public void testSubruleInstantiation_outsideAllowlist_failsWithPrivateAPIError()
      throws Exception {
    evOutsideAllowlist.checkEvalErrorContains(
        "'//foo:bar' cannot use private API", "subrule(implementation = lambda: 0 )");
  }

  @Test
  public void testSubrulesParamForRule_isPrivateAPI() throws Exception {
    evOutsideAllowlist.checkEvalErrorContains(
        "'//foo:bar' cannot use private API", "rule(implementation = lambda: 0, subrules = [1])");
  }

  @Test
  public void testSubrulesParamForAspect_isPrivateAPI() throws Exception {
    evOutsideAllowlist.checkEvalErrorContains(
        "'//foo:bar' cannot use private API", "aspect(implementation = lambda: 0, subrules = [1])");
  }

  private StructImpl getProvider(String targetLabel, String providerLabel, String providerName)
      throws LabelSyntaxException {
    ConfiguredTarget target = getConfiguredTarget(targetLabel);
    Provider.Key key = new StarlarkProvider.Key(Label.parseCanonical(providerLabel), providerName);
    return (StructImpl) target.get(key);
  }
}
