Add basic builtins injection functionality, behind a flag

BzlLoadFunction now requests the injected builtins from StarlarkBuiltinsFunction. Added BuildViewTestCase tests of injection. Improved error handling is in a follow-up CL.

Other changes:
- Add cautionary note about BuildViewTestCase#getConfiguredTarget returning null.

Work toward #11437.

RELNOTES: None
PiperOrigin-RevId: 315115168
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java
index c1a3990..d8f9acf 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java
@@ -92,8 +92,9 @@
   // instead accept the set of predeclared bindings. Simplify the code path and then this comment.
   private final RuleClassProvider ruleClassProvider;
 
-  // TODO(#11437): Remove once we're getting builtins from StarlarkBuiltinsValue instead.
-  private final ImmutableMap<String, Object> predeclaredForBuildBzl;
+  // Used for BUILD .bzls if injection is disabled.
+  // TODO(#11437): Remove once injection is on unconditionally.
+  private final StarlarkBuiltinsValue uninjectedStarlarkBuiltins;
 
   private final ImmutableMap<String, Object> predeclaredForWorkspaceBzl;
 
@@ -115,12 +116,9 @@
       ASTManager astManager,
       @Nullable CachedBzlLoadDataManager cachedBzlLoadDataManager) {
     this.ruleClassProvider = ruleClassProvider;
-    this.predeclaredForBuildBzl =
-        StarlarkBuiltinsFunction.createPredeclaredForBuildBzlUsingInjection(
-            ruleClassProvider,
-            packageFactory,
-            /*exportedToplevels=*/ ImmutableMap.of(),
-            /*exportedRules=*/ ImmutableMap.of());
+    this.uninjectedStarlarkBuiltins =
+        StarlarkBuiltinsFunction.createStarlarkBuiltinsValueWithoutInjection(
+            ruleClassProvider, packageFactory);
     this.predeclaredForWorkspaceBzl =
         StarlarkBuiltinsFunction.createPredeclaredForWorkspaceBzl(
             ruleClassProvider, packageFactory);
@@ -338,6 +336,27 @@
     cachedBzlLoadDataManager.reset();
   }
 
+  /**
+   * Retrieves {@link StarlarkBuiltinsValue}, or uses a dummy value if either 1) the builtins
+   * mechanism is disabled or 2) the current .bzl is not being loaded for a BUILD file
+   *
+   * <p>Returns null if a Skyframe restart/error occurred.
+   */
+  @Nullable
+  private StarlarkBuiltinsValue getStarlarkBuiltinsValue(
+      BzlLoadValue.Key key, Environment env, StarlarkSemantics starlarkSemantics)
+      throws InterruptedException {
+    // Don't request @builtins if we're in workspace evaluation, or already in @builtins evaluation.
+    if (!(key instanceof BzlLoadValue.KeyForBuild)
+        // TODO(#11437): Remove ability to disable injection by setting flag to empty string.
+        || starlarkSemantics.experimentalBuiltinsBzlPath().equals("")) {
+      return uninjectedStarlarkBuiltins;
+    }
+    // May be null.
+    return (StarlarkBuiltinsValue) env.getValue(StarlarkBuiltinsValue.key());
+  }
+
+  @Nullable
   private static ContainingPackageLookupValue getContainingPackageLookupValue(
       Environment env, Label label)
       throws InconsistentFilesystemException, BzlLoadFailedException, InterruptedException {
@@ -443,6 +462,12 @@
       return null;
     }
 
+    StarlarkBuiltinsValue starlarkBuiltinsValue =
+        getStarlarkBuiltinsValue(key, env, starlarkSemantics);
+    if (starlarkBuiltinsValue == null) {
+      return null;
+    }
+
     if (getContainingPackageLookupValue(env, label) == null) {
       return null;
     }
@@ -462,7 +487,13 @@
     try {
       result =
           computeInternalWithAST(
-              key, filePath, starlarkSemantics, astLookupValue, env, inliningState);
+              key,
+              filePath,
+              starlarkSemantics,
+              starlarkBuiltinsValue,
+              astLookupValue,
+              env,
+              inliningState);
     } catch (InconsistentFilesystemException | BzlLoadFailedException | InterruptedException e) {
       astManager.doneWithASTFileLookupValue(label);
       throw e;
@@ -479,6 +510,7 @@
       BzlLoadValue.Key key,
       PathFragment filePath,
       StarlarkSemantics starlarkSemantics,
+      StarlarkBuiltinsValue starlarkBuiltinsValue,
       ASTFileLookupValue astLookupValue,
       Environment env,
       @Nullable InliningState inliningState)
@@ -540,7 +572,9 @@
     }
     byte[] transitiveDigest = fp.digestAndReset();
 
-    Module module = Module.withPredeclared(starlarkSemantics, getPredeclaredEnvironment(key));
+    Module module =
+        Module.withPredeclared(
+            starlarkSemantics, getPredeclaredEnvironment(key, starlarkBuiltinsValue));
     module.setClientData(BazelModuleContext.create(label, transitiveDigest));
     // executeBzlFile may post events to the Environment's handler, but events do not matter when
     // caching BzlLoadValues. Note that executing the module mutates it.
@@ -693,6 +727,11 @@
     Environment strippedEnv = ((RecordingSkyFunctionEnvironment) env).getDelegate();
 
     List<BzlLoadValue> bzlLoads = Lists.newArrayListWithExpectedSize(keys.size());
+    // For determinism's sake while inlining, preserve the first exception and continue to run
+    // subsequently listed loads to completion/exception, loading all transitive deps anyway.
+    // TODO(brandjon, adgar): What's the deal with determinism w.r.t. InterruptedException, and
+    // don't null returns mean the first exception seen isn't deterministic? See also cl/263656490
+    // and discussion therein.
     Exception deferredException = null;
     boolean valuesMissing = false;
     // NOTE: Iterating over loads in the order listed in the file.
@@ -701,8 +740,6 @@
       try {
         cachedData = computeInlineWithState(key, strippedEnv, inliningState);
       } catch (BzlLoadFailedException | InconsistentFilesystemException e) {
-        // For determinism's sake while inlining, preserve the first exception and continue to run
-        // subsequently listed loads to completion/exception, loading all transitive deps anyway.
         deferredException = MoreObjects.firstNonNull(deferredException, e);
         continue;
       }
@@ -729,9 +766,10 @@
    * Obtains the predeclared environment for a .bzl file, based on the type of file and (if
    * applicable) the injected builtins.
    */
-  private ImmutableMap<String, Object> getPredeclaredEnvironment(BzlLoadValue.Key key) {
+  private ImmutableMap<String, Object> getPredeclaredEnvironment(
+      BzlLoadValue.Key key, StarlarkBuiltinsValue starlarkBuiltinsValue) {
     if (key instanceof BzlLoadValue.KeyForBuild) {
-      return predeclaredForBuildBzl;
+      return starlarkBuiltinsValue.predeclaredForBuildBzl;
     } else if (key instanceof BzlLoadValue.KeyForWorkspace) {
       return predeclaredForWorkspaceBzl;
     } else if (key instanceof BzlLoadValue.KeyForBuiltins) {
@@ -860,6 +898,8 @@
     static BzlLoadFailedException skylarkErrors(PathFragment file) {
       return new BzlLoadFailedException(String.format("Extension '%s' has errors", file));
     }
+
+    // TODO(#11437): Add builtinsFailed() factory method here.
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/StarlarkBuiltinsFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/StarlarkBuiltinsFunction.java
index a610a56..1b773f5 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/StarlarkBuiltinsFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/StarlarkBuiltinsFunction.java
@@ -136,9 +136,7 @@
    * Returns the set of predeclared symbols to initialize a Starlark {@link Module} with, for
    * evaluating .bzls loaded from a BUILD file.
    */
-  // TODO(#11437): Make private in follow-up CL to retrieve predeclared environment from
-  // StarlarkBuiltinsValue instead of this static helper.
-  static ImmutableMap<String, Object> createPredeclaredForBuildBzlUsingInjection(
+  private static ImmutableMap<String, Object> createPredeclaredForBuildBzlUsingInjection(
       RuleClassProvider ruleClassProvider,
       PackageFactory packageFactory,
       ImmutableMap<String, Object> exportedToplevels,
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
index b4e8ee4..11a3b83 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
@@ -879,7 +879,10 @@
    * Returns the ConfiguredTarget for the specified label, configured for the "build" (aka "target")
    * configuration. If the label corresponds to a target with a top-level configuration transition,
    * that transition is applied to the given config in the returned ConfiguredTarget.
+   *
+   * <p>May return null on error; see {@link #getConfiguredTarget(Label, BuildConfiguration)}.
    */
+  @Nullable
   public ConfiguredTarget getConfiguredTarget(String label) throws LabelSyntaxException {
     return getConfiguredTarget(label, targetConfig);
   }
@@ -888,7 +891,10 @@
    * Returns the ConfiguredTarget for the specified label, using the given build configuration. If
    * the label corresponds to a target with a top-level configuration transition, that transition is
    * applied to the given config in the returned ConfiguredTarget.
+   *
+   * <p>May return null on error; see {@link #getConfiguredTarget(Label, BuildConfiguration)}.
    */
+  @Nullable
   protected ConfiguredTarget getConfiguredTarget(String label, BuildConfiguration config)
       throws LabelSyntaxException {
     return getConfiguredTarget(Label.parseAbsolute(label, ImmutableMap.of()), config);
@@ -906,6 +912,9 @@
    *
    * @throws AssertionError if the target cannot be transitioned into with the given configuration
    */
+  // TODO(bazel-team): Should we work around b/26382502 by asserting here that the result is not
+  // null?
+  @Nullable
   protected ConfiguredTarget getConfiguredTarget(Label label, BuildConfiguration config) {
     try {
       return view.getConfiguredTargetForTesting(
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BuiltinsInjectionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/BuiltinsInjectionTest.java
new file mode 100644
index 0000000..660090d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BuiltinsInjectionTest.java
@@ -0,0 +1,215 @@
+// Copyright 2020 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.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.analysis.util.MockRule;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Starlark builtin injection.
+ *
+ * <p>Essentially these are integration tests between {@link StarlarkBuiltinsFunction} and {@link
+ * BzlLoadFunction}.
+ */
+@RunWith(JUnit4.class)
+public class BuiltinsInjectionTest extends BuildViewTestCase {
+
+  private static final MockRule OVERRIDABLE_RULE = () -> MockRule.define("overridable_rule");
+
+  @Override
+  protected ConfiguredRuleClassProvider getRuleClassProvider() {
+    // Add a fake rule and top-level symbol to override.
+    ConfiguredRuleClassProvider.Builder builder =
+        new ConfiguredRuleClassProvider.Builder()
+            .addRuleDefinition(OVERRIDABLE_RULE)
+            .addStarlarkAccessibleTopLevels("overridable_symbol", "original_value");
+    TestRuleClassProvider.addStandardRules(builder);
+    return builder.build();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    setStarlarkSemanticsOptions("--experimental_builtins_bzl_path=notdisabled");
+  }
+
+  /**
+   * Writes an exports.bzl file with the given content, along with a BUILD file, in the builtins
+   * location.
+   *
+   * <p>See {@link StarlarkBuiltinsFunction#EXPORTS_ENTRYPOINT} for the significance of exports.bzl.
+   */
+  // TODO(#11437): Don't write the BUILD file once it's no longer needed. Pass staging location into
+  // test setup above.
+  private void writeExportsBzl(String... lines) throws Exception {
+    scratch.file("tools/builtins_staging/BUILD");
+    scratch.file("tools/builtins_staging/exports.bzl", lines);
+  }
+
+  /**
+   * Writes a pkg/dummy.bzl file, and an accompanying BUILD file that loads it and defines a
+   * //pkg:dummy target.
+   */
+  private void writePkgBzl(String... lines) throws Exception {
+    scratch.file(
+        "pkg/BUILD",
+        "load(':dummy.bzl', 'dummy_symbol')",
+        // We define :dummy as a simple file rather than something like a java_library, because
+        // native rules may depend on exports.bzl, which we are messing with in this test suite.
+        "exports_files(['dummy'])");
+    scratch.file("pkg/dummy");
+    List<String> modifiedLines = new ArrayList<>(Arrays.asList(lines));
+    modifiedLines.add(0, "dummy_symbol = None");
+    scratch.file("pkg/dummy.bzl", modifiedLines.toArray(lines));
+  }
+
+  /**
+   * Builds {@code //pkg:dummy} and asserts success.
+   *
+   * <p>This helps us fail fast when {@link #getConfiguredTarget} returns null without emitting
+   * events; see b/26382502.
+   */
+  private void buildDummyAndAssertSuccess() throws Exception {
+    Object result = getConfiguredTarget("//pkg:dummy");
+    assertThat(result).isNotNull();
+  }
+
+  /** Builds {@code //pkg:dummy}, which may be in error. */
+  private void buildDummyWithoutAssertingSuccess() throws Exception {
+    getConfiguredTarget("//pkg:dummy");
+  }
+
+  @Test
+  public void evalWithInjection() throws Exception {
+    writeExportsBzl(
+        "exported_toplevels = {'overridable_symbol': 'new_value'}",
+        "exported_rules = {'overridable_rule': 'new_rule'}",
+        "exported_to_java = {}");
+    writePkgBzl(
+        "print('overridable_symbol :: ' + str(overridable_symbol))",
+        "print('overridable_rule :: ' + str(native.overridable_rule))");
+
+    buildDummyAndAssertSuccess();
+    assertContainsEvent("overridable_symbol :: new_value");
+    assertContainsEvent("overridable_rule :: new_rule");
+  }
+
+  // TODO(#11437): Remove once disabling is not allowed.
+  @Test
+  public void injectionDisabledByFlag() throws Exception {
+    writeExportsBzl(
+        "exported_toplevels = {'overridable_symbol': 'new_value'}",
+        "exported_rules = {'overridable_rule': 'new_rule'}",
+        "exported_to_java = {}");
+    writePkgBzl(
+        "print('overridable_symbol :: ' + str(overridable_symbol))",
+        "print('overridable_rule :: ' + str(native.overridable_rule))");
+    setStarlarkSemanticsOptions("--experimental_builtins_bzl_path=");
+
+    buildDummyAndAssertSuccess();
+    assertContainsEvent("overridable_symbol :: original_value");
+    assertContainsEvent("overridable_rule :: <built-in rule overridable_rule>");
+  }
+
+  // TODO(#11437): Remove once disabling is not allowed.
+  @Test
+  public void exportsBzlMayBeInErrorWhenInjectionIsDisabled() throws Exception {
+    writeExportsBzl( //
+        "PARSE ERROR");
+    writePkgBzl(
+        "print('overridable_symbol :: ' + str(overridable_symbol))",
+        "print('overridable_rule :: ' + str(native.overridable_rule))");
+    setStarlarkSemanticsOptions("--experimental_builtins_bzl_path=");
+
+    buildDummyAndAssertSuccess();
+    assertContainsEvent("overridable_symbol :: original_value");
+    assertContainsEvent("overridable_rule :: <built-in rule overridable_rule>");
+  }
+
+  @Test
+  public void evalWorkspaceBzl_doesNotUseInjection() throws Exception {
+    writeExportsBzl(
+        "exported_toplevels = {'overridable_symbol': 'new_value'}",
+        "exported_rules = {'overridable_rule': 'new_rule'}",
+        "exported_to_java = {}");
+    writePkgBzl();
+    scratch.appendFile(
+        "WORKSPACE", //
+        "load(':foo.bzl', 'dummy_symbol')",
+        "print(dummy_symbol)");
+    scratch.file("BUILD");
+    scratch.file(
+        "foo.bzl",
+        "dummy_symbol = None",
+        "print('overridable_symbol :: ' + str(overridable_symbol))");
+
+    buildDummyAndAssertSuccess();
+    // Builtins for WORKSPACE bzls are populated essentially the same as for BUILD bzls, except that
+    // injection doesn't apply.
+    assertContainsEvent("overridable_symbol :: original_value");
+    // We don't assert that the rule isn't injected because the workspace native object doesn't
+    // contain our original mock rule. We can test this for WORKSPACE files at the top-level though.
+  }
+
+  // TODO(#11437): Broaden to also assert that it can't access rule-specific symbols (like our
+  // override symbols) but can still access basic symbols (print(), rule()).
+  @Test
+  public void evalBuiltinsBzl_cannotAccessNative() throws Exception {
+    writeExportsBzl(
+        // The load string here references a regular label, but it gets loaded using a
+        // BzlLoadValue.KeyForBuiltins because we're in exports.bzl.
+        // TODO(#11437): Rewrite this load syntax to @builtins//...
+        "load('//pkg:builtins_dummy.bzl', 'builtins_dummy')",
+        "exported_toplevels = {'overridable_symbol': 'new_value'}",
+        "exported_rules = {'overridable_rule': 'new_rule'}",
+        "exported_to_java = {}");
+    writePkgBzl();
+    scratch.file(
+        "pkg/builtins_dummy.bzl", //
+        "builtins_dummy = None",
+        "print('overridable_symbol :: ' + str(overridable_symbol))",
+        "print('overridable_rule :: ' + str(native.overridable_rule))");
+    reporter.removeHandler(failFastHandler);
+
+    buildDummyWithoutAssertingSuccess();
+    // Currently overridable_symbol, a piece of (mock) rule logic, is accessible, but a future
+    // change will prevent that.
+    assertContainsEvent("overridable_symbol :: original_value");
+    // Currently, the "native" object actually exists, but isn't fully formed -- it contains generic
+    // methods but not specific rules. We will change this to be completely absent from @builtins.
+    assertContainsEvent("'native' value has no field or method 'overridable_rule'");
+  }
+
+  // TODO(#11437): Reject overriding symbols that don't already exist. Reject overriding "native".
+  // Reject overriding non-rule-logic symbols such as package(), select(), environment_extension,
+  // varref(), etc.
+
+  // TODO(#11437): Add tests that the _internal symbol is accessible nowhere but builtins bzls.
+
+  // TODO(#11437): Add tests of the _internal symbol's usage within builtins bzls.
+
+  // TODO(#11437): Add test cases for BUILD file injection, and WORKSPACE file non-injection, when
+  // we add injection support to PackageFunction.
+}