Create infrastructure to restrict top-level Starlark objects by flag

As proof of concept, this restricts AnalysisFailureInfo and AnalysisTestResultInfo to be unavailable as top-level symbols without --experimental_analysis_testing_improvements . This is technically a breaking change, but these symbols were unusable before this change, documented as being experimental, and are not included in any binary release of Bazel.

RELNOTES: None.
PiperOrigin-RevId: 217593936
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/ActionsInfoProviderApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/ActionsInfoProviderApi.java
index d19086a..8c21ebc 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/ActionsInfoProviderApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/ActionsInfoProviderApi.java
@@ -21,7 +21,7 @@
  * Provider for structs containing actions created during the analysis of a rule.
  */
 @SkylarkModule(name = "Actions",
-    doc = "",
+    doc = "<b>Deprecated and subject to imminent removal. Please do not use.</b>",
     documented = false,
     category = SkylarkModuleCategory.PROVIDER)
 // TODO(cparsons): Deprecate and remove this API.
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/TestingBootstrap.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/TestingBootstrap.java
index 745df17..f8d5a4b 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/TestingBootstrap.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/TestingBootstrap.java
@@ -18,6 +18,8 @@
 import com.google.devtools.build.lib.skylarkbuildapi.Bootstrap;
 import com.google.devtools.build.lib.skylarkbuildapi.test.AnalysisFailureInfoApi.AnalysisFailureInfoProviderApi;
 import com.google.devtools.build.lib.skylarkbuildapi.test.AnalysisTestResultInfoApi.AnalysisTestResultInfoProviderApi;
+import com.google.devtools.build.lib.syntax.FlagGuardedValue;
+import com.google.devtools.build.lib.syntax.SkylarkSemantics.FlagIdentifier;
 
 /**
  * {@link Bootstrap} for skylark objects related to testing.
@@ -39,7 +41,13 @@
   @Override
   public void addBindingsToBuilder(ImmutableMap.Builder<String, Object> builder) {
     builder.put("testing", testingModule);
-    builder.put("AnalysisFailureInfo", analysisFailureInfoProvider);
-    builder.put("AnalysisTestResultInfo", testResultInfoProvider);
+    builder.put("AnalysisFailureInfo",
+         FlagGuardedValue.onlyWhenExperimentalFlagIsTrue(
+             FlagIdentifier.EXPERIMENTAL_ANALYSIS_TESTING_IMPROVEMENTS,
+           analysisFailureInfoProvider));
+    builder.put("AnalysisTestResultInfo",
+        FlagGuardedValue.onlyWhenExperimentalFlagIsTrue(
+            FlagIdentifier.EXPERIMENTAL_ANALYSIS_TESTING_IMPROVEMENTS,
+        testResultInfoProvider));
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
index 8e01e89..f0957a3 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
@@ -41,6 +41,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
@@ -272,6 +273,14 @@
     /** Bindings are maintained in order of creation. */
     private final LinkedHashMap<String, Object> bindings;
 
+    /**
+     * A list of bindings which *would* exist in this global frame under certain semantic
+     * flags, but do not exist using the semantic flags used in this frame's creation.
+     * This map should not be used for lookups; it should only be used to throw descriptive
+     * error messages when a lookup of a restricted object is attempted.
+     **/
+    private final LinkedHashMap<String, FlagGuardedValue> restrictedBindings;
+
     /** Set of bindings that are exported (can be loaded from other modules). */
     private final HashSet<String> exportedBindings;
 
@@ -281,6 +290,7 @@
       this.universe = null;
       this.label = null;
       this.bindings = new LinkedHashMap<>();
+      this.restrictedBindings = new LinkedHashMap<>();
       this.exportedBindings = new HashSet<>();
     }
 
@@ -288,7 +298,8 @@
         Mutability mutability,
         @Nullable GlobalFrame universe,
         @Nullable Label label,
-        @Nullable Map<String, Object> bindings) {
+        @Nullable Map<String, Object> bindings,
+        @Nullable Map<String, FlagGuardedValue> restrictedBindings) {
       Preconditions.checkState(universe == null || universe.universe == null);
       this.mutability = Preconditions.checkNotNull(mutability);
       this.universe = universe;
@@ -303,26 +314,68 @@
       if (bindings != null) {
         this.bindings.putAll(bindings);
       }
+      this.restrictedBindings = new LinkedHashMap<>();
+      if (restrictedBindings != null) {
+        this.restrictedBindings.putAll(restrictedBindings);
+      }
+      if (universe != null) {
+        this.restrictedBindings.putAll(universe.restrictedBindings);
+      }
       this.exportedBindings = new HashSet<>();
     }
 
     public GlobalFrame(Mutability mutability) {
-      this(mutability, null, null, null);
+      this(mutability, null, null, null, null);
     }
 
     public GlobalFrame(Mutability mutability, @Nullable GlobalFrame universe) {
-      this(mutability, universe, null, null);
+      this(mutability, universe, null, null, null);
     }
 
     public GlobalFrame(
         Mutability mutability, @Nullable GlobalFrame universe, @Nullable Label label) {
-      this(mutability, universe, label, null);
+      this(mutability, universe, label, null, null);
     }
 
     /** Constructs a global frame for the given builtin bindings. */
     public static GlobalFrame createForBuiltins(Map<String, Object> bindings) {
       Mutability mutability = Mutability.create("<builtins>").freeze();
-      return new GlobalFrame(mutability, null, null, bindings);
+      return new GlobalFrame(mutability, null, null, bindings, null);
+    }
+
+    /**
+     * Constructs a global frame based on the given parent frame, filtering out flag-restricted
+     * global objects.
+     */
+    public static GlobalFrame filterOutRestrictedBindings(
+        Mutability mutability, GlobalFrame parent, SkylarkSemantics semantics) {
+      if (parent == null) {
+        return new GlobalFrame(mutability);
+      }
+      Map<String, Object> filteredBindings = new LinkedHashMap<>();
+      Map<String, FlagGuardedValue> restrictedBindings = new LinkedHashMap<>();
+
+      for (Entry<String, Object> binding : parent.getTransitiveBindings().entrySet()) {
+        if (binding.getValue() instanceof FlagGuardedValue) {
+          FlagGuardedValue val = (FlagGuardedValue) binding.getValue();
+          if (val.isObjectAccessibleUsingSemantics(semantics)) {
+            filteredBindings.put(binding.getKey(), val.getObject(semantics));
+          } else {
+            restrictedBindings.put(binding.getKey(), val);
+          }
+        } else {
+          filteredBindings.put(binding.getKey(), binding.getValue());
+        }
+      }
+
+      restrictedBindings.putAll(parent.restrictedBindings);
+
+      return new GlobalFrame(
+          mutability,
+          null /*parent */,
+          parent.label,
+          filteredBindings,
+          restrictedBindings);
     }
 
     private void checkInitialized() {
@@ -356,7 +409,8 @@
      */
     public GlobalFrame withLabel(Label label) {
       checkInitialized();
-      return new GlobalFrame(mutability, /*universe*/ null, label, bindings);
+      return new GlobalFrame(mutability, /*universe*/ null, label, bindings,
+          /*restrictedBindings*/ null);
     }
 
     /** Returns the {@link Mutability} of this {@link GlobalFrame}. */
@@ -928,6 +982,9 @@
     /** Builds the Environment. */
     public Environment build() {
       Preconditions.checkArgument(!mutability.isFrozen());
+      if (semantics == null) {
+        throw new IllegalArgumentException("must call either setSemantics or useDefaultSemantics");
+      }
       if (parent != null) {
         Preconditions.checkArgument(parent.mutability().isFrozen(), "parent frame must be frozen");
         if (parent.universe != null) { // This code path doesn't happen in Bazel.
@@ -938,14 +995,20 @@
                   parent.mutability(),
                   null /* parent */,
                   parent.label,
-                  parent.getTransitiveBindings());
+                  parent.getTransitiveBindings(),
+                  parent.restrictedBindings);
         }
       }
+
+      // Filter out restricted objects from the universe scope. This cannot be done in-place in
+      // creation of the input global universe scope, because this environment's semantics may not
+      // have been available during its creation. Thus, create a new universe scope for this
+      // environment which is equivalent in every way except that restricted bindings are
+      // filtered out.
+      parent = GlobalFrame.filterOutRestrictedBindings(mutability, parent, semantics);
+
       GlobalFrame globalFrame = new GlobalFrame(mutability, parent);
       LexicalFrame dynamicFrame = LexicalFrame.create(mutability);
-      if (semantics == null) {
-        throw new IllegalArgumentException("must call either setSemantics or useDefaultSemantics");
-      }
       if (importedExtensions == null) {
         importedExtensions = ImmutableMap.of();
       }
@@ -1143,6 +1206,16 @@
   }
 
   /**
+   * Returns a map containing all bindings that are technically <i>present</i> but are
+   * <i>restricted</i> in the current frame with the current semantics. Such bindings should be
+   * treated unresolvable; this method should be invoked to prepare error messaging for
+   * evaluation environments where access of these restricted objects may have been attempted.
+   */
+  public Map<String, FlagGuardedValue> getRestrictedBindings() {
+    return globalFrame.restrictedBindings;
+  }
+
+  /**
    * Returns true if varname is a known global variable (i.e., it has been read in the context of
    * the current function).
    */
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FlagGuardedValue.java b/src/main/java/com/google/devtools/build/lib/syntax/FlagGuardedValue.java
new file mode 100644
index 0000000..e21c23d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FlagGuardedValue.java
@@ -0,0 +1,119 @@
+// Copyright 2018 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.SkylarkSemantics.FlagIdentifier;
+
+/**
+ * Wrapper on a value that controls its accessibility in Starlark based on the value of a
+ * semantic flag.
+ *
+ * <p>For example, this could control whether symbol "Foo" exists in the Starlark
+ * global frame: such a symbol might only be accessible if --experimental_foo is set to true.
+ * In order to create this control, an instance of this class should be added to the global
+ * frame under "Foo". This flag guard will throw a descriptive {@link EvalException} when
+ * "Foo" would be accessed without the proper flag.
+ */
+public class FlagGuardedValue {
+  private final Object obj;
+  private final FlagIdentifier flagIdentifier;
+  private final FlagType flagType;
+
+  private enum FlagType {
+    DEPRECATION,
+    EXPERIMENTAL;
+  }
+
+  private FlagGuardedValue(Object obj, FlagIdentifier flagIdentifier, FlagType flagType) {
+    this.obj = obj;
+    this.flagIdentifier = flagIdentifier;
+    this.flagType = flagType;
+  }
+
+  /**
+   * Creates a flag guard which only permits access of the given object when the given flag is
+   * true. If the given flag is false and the object would be accessed, an error is thrown
+   * describing the feature as experimental, and describing that the flag must be set to true.
+   */
+  public static FlagGuardedValue onlyWhenExperimentalFlagIsTrue(
+      FlagIdentifier flagIdentifier, Object obj) {
+    return new FlagGuardedValue(obj, flagIdentifier, FlagType.EXPERIMENTAL);
+  }
+
+  /**
+   * Creates a flag guard which only permits access of the given object when the given flag is
+   * false. If the given flag is true and the object would be accessed, an error is thrown
+   * describing the feature as deprecated, and describing that the flag must be set to false.
+   */
+  public static FlagGuardedValue onlyWhenIncompatibleFlagIsFalse(
+      FlagIdentifier flagIdentifier, Object obj) {
+    return new FlagGuardedValue(obj, flagIdentifier, FlagType.DEPRECATION);
+  }
+
+  /**
+   * Returns an {@link EvalException} with error appropriate to throw when one attempts to
+   * access this guard's protected object when it should be inaccessible in the given semantics.
+   *
+   * @throws IllegalArgumentException if {@link #isObjectAccessibleUsingSemantics} is true
+   *     given the semantics
+   */
+  public EvalException getEvalExceptionFromAttemptingAccess(
+      Location location, SkylarkSemantics semantics, String symbolDescription) {
+    Preconditions.checkArgument(!isObjectAccessibleUsingSemantics(semantics),
+        "getEvalExceptionFromAttemptingAccess should only be called if the underlying "
+            + "object is inaccessible given the semantics");
+    if (flagType == FlagType.EXPERIMENTAL) {
+      return new EvalException(
+            location,
+            symbolDescription
+                + " is experimental and thus unavailable with the current flags. It may be "
+                + "enabled by setting --" + flagIdentifier.getFlagName());
+    } else {
+      return new EvalException(
+        location,
+        symbolDescription
+            + " is deprecated and will be removed soon. It may be temporarily re-enabled by "
+            + "setting --" + flagIdentifier.getFlagName() + "=false");
+
+    }
+  }
+
+  /**
+   * Returns this guard's underlying object. This should be called when appropriate validation
+   * has occurred to ensure that the object is accessible with the given semantics.
+   *
+   * @throws IllegalArgumentException if {@link #isObjectAccessibleUsingSemantics} is false
+   *     given the semantics
+   */
+  public Object getObject(SkylarkSemantics semantics) {
+    Preconditions.checkArgument(isObjectAccessibleUsingSemantics(semantics),
+        "getObject should only be called if the underlying object is accessible given the "
+            + "semantics");
+    return obj;
+  }
+
+  /**
+   * Returns true if this guard's underlying object is accessible under the given semantics.
+   */
+  public boolean isObjectAccessibleUsingSemantics(SkylarkSemantics semantics) {
+    if (flagType == FlagType.EXPERIMENTAL) {
+      return semantics.isFeatureEnabledBasedOnTogglingFlags(flagIdentifier, FlagIdentifier.NONE);
+    } else {
+      return semantics.isFeatureEnabledBasedOnTogglingFlags(FlagIdentifier.NONE, flagIdentifier);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkSemantics.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkSemantics.java
index 0240a67..3605c61 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkSemantics.java
@@ -33,7 +33,10 @@
 @AutoValue
 public abstract class SkylarkSemantics {
 
-  /** Enum where each element represents a skylark semantics flag. */
+  /**
+   * Enum where each element represents a skylark semantics flag. The name of each value should
+   * be the exact name of the flag transformed to upper case (for error representation).
+   */
   public enum FlagIdentifier {
     EXPERIMENTAL_ANALYSIS_TESTING_IMPROVEMENTS(
         SkylarkSemantics::experimentalAnalysisTestingImprovements),
@@ -52,6 +55,14 @@
     FlagIdentifier(Function<SkylarkSemantics, Boolean> semanticsFunction) {
       this.semanticsFunction = semanticsFunction;
     }
+
+    /**
+     * Returns the name of the flag that this identifier controls. For example, EXPERIMENTAL_FOO
+     * would return 'experimental_foo'.
+     */
+    public String getFlagName() {
+      return this.name().toLowerCase();
+    }
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
index 8900e5f..39744fe 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
@@ -85,14 +85,14 @@
     }
   }
 
-  private final SkylarkSemantics semantics;
+  private final Environment env;
   private Block block;
   private int loopCount;
 
   /** Create a ValidationEnvironment for a given global Environment (containing builtins). */
   ValidationEnvironment(Environment env) {
     Preconditions.checkArgument(env.isGlobal());
-    semantics = env.getSemantics();
+    this.env = env;
     block = new Block(Scope.Universe, null);
     Set<String> builtinVariables = env.getVariableNames();
     block.variables.addAll(builtinVariables);
@@ -157,6 +157,13 @@
   public void visit(Identifier node) {
     @Nullable Block b = blockThatDefines(node.getName());
     if (b == null) {
+      // The identifier might not exist because it was restricted (hidden) by the current semantics.
+      // If this is the case, output a more helpful error message than 'not found'.
+      FlagGuardedValue result = env.getRestrictedBindings().get(node.getName());
+      if (result != null) {
+        throw new ValidationException(result.getEvalExceptionFromAttemptingAccess(
+            node.getLocation(), env.getSemantics(), node.getName()));
+      }
       throw new ValidationException(node.createInvalidIdentifierException(getAllSymbols()));
     }
     node.setScope(b.scope);
@@ -326,7 +333,7 @@
   /** Validates the AST and runs static checks. */
   private void validateAst(List<Statement> statements) {
     // Check that load() statements are on top.
-    if (semantics.incompatibleBzlDisallowLoadAfterStatement()) {
+    if (env.getSemantics().incompatibleBzlDisallowLoadAfterStatement()) {
       checkLoadAfterStatement(statements);
     }
 
diff --git a/src/test/java/com/google/devtools/build/docgen/SkylarkDocumentationTest.java b/src/test/java/com/google/devtools/build/docgen/SkylarkDocumentationTest.java
index 412a74e..16c0aa8 100644
--- a/src/test/java/com/google/devtools/build/docgen/SkylarkDocumentationTest.java
+++ b/src/test/java/com/google/devtools/build/docgen/SkylarkDocumentationTest.java
@@ -18,7 +18,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.devtools.build.docgen.skylark.SkylarkBuiltinMethodDoc;
 import com.google.devtools.build.docgen.skylark.SkylarkJavaMethodDoc;
 import com.google.devtools.build.docgen.skylark.SkylarkMethodDoc;
 import com.google.devtools.build.docgen.skylark.SkylarkModuleDoc;
@@ -87,9 +86,8 @@
     Map<String, SkylarkModuleDoc> modules = SkylarkDocumentationCollector.collectModules();
     SkylarkModuleDoc topLevel =
         modules.remove(SkylarkDocumentationCollector.getTopLevelModule().name());
-    for (Map.Entry<String, SkylarkBuiltinMethodDoc> entry :
-        topLevel.getBuiltinMethods().entrySet()) {
-      docMap.put(entry.getKey(), entry.getValue().getDocumentation());
+    for (SkylarkMethodDoc method : topLevel.getMethods()) {
+      docMap.put(method.getName(), method.getDocumentation());
     }
     for (Map.Entry<String, SkylarkModuleDoc> entry : modules.entrySet()) {
       docMap.put(entry.getKey(), entry.getValue().getDocumentation());
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
index 92b16ec..d89f9fb 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
@@ -2119,7 +2119,9 @@
 
     reporter.removeHandler(failFastHandler);
     getConfiguredTarget("//test:r");
-    assertContainsEvent("'Provider' object is not callable");
+    assertContainsEvent(
+        "AnalysisTestResultInfo is experimental and thus unavailable with the current flags. "
+            + "It may be enabled by setting --experimental_analysis_testing_improvements");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index bec987f..071019e 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -36,6 +36,7 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
+import com.google.devtools.build.lib.syntax.SkylarkSemantics.FlagIdentifier;
 import com.google.devtools.build.lib.testutil.TestMode;
 import java.util.List;
 import java.util.Map;
@@ -2296,4 +2297,80 @@
         .testIfErrorContains("'AnalysisFailure' has no field 'message'", "val.message")
         .testIfErrorContains("'AnalysisFailure' has no field 'label'", "val.label");
   }
+
+  @Test
+  public void testExperimentalFlagGuardedValue() throws Exception {
+    // This test uses an arbitrary experimental flag to verify this functionality. If this
+    // experimental flag were to go away, this test may be updated to use any experimental flag.
+    // The flag itself is unimportant to the test.
+    FlagGuardedValue val = FlagGuardedValue.onlyWhenExperimentalFlagIsTrue(
+        FlagIdentifier.EXPERIMENTAL_ANALYSIS_TESTING_IMPROVEMENTS,
+        "foo");
+    String errorMessage = "GlobalSymbol is experimental and thus unavailable with the current "
+        + "flags. It may be enabled by setting --experimental_analysis_testing_improvements";
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--experimental_analysis_testing_improvements=true")
+        .setUp("var = GlobalSymbol")
+        .testLookup("var", "foo");
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--experimental_analysis_testing_improvements=false")
+        .testIfErrorContains(errorMessage,
+            "var = GlobalSymbol");
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--experimental_analysis_testing_improvements=false")
+        .testIfErrorContains(errorMessage,
+            "def my_function():",
+            "  var = GlobalSymbol");
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--experimental_analysis_testing_improvements=false")
+        .setUp("GlobalSymbol = 'other'",
+            "var = GlobalSymbol")
+        .testLookup("var", "other");
+  }
+
+  @Test
+  public void testIncompatibleFlagGuardedValue() throws Exception {
+    // This test uses an arbitrary incompatible flag to verify this functionality. If this
+    // incompatible flag were to go away, this test may be updated to use any incompatible flag.
+    // The flag itself is unimportant to the test.
+    FlagGuardedValue val = FlagGuardedValue.onlyWhenIncompatibleFlagIsFalse(
+        FlagIdentifier.INCOMPATIBLE_NO_TARGET_OUTPUT_GROUP,
+        "foo");
+    String errorMessage = "GlobalSymbol is deprecated and will be removed soon. It may be "
+        + "temporarily re-enabled by setting --incompatible_no_target_output_group=false";
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--incompatible_no_target_output_group=false")
+        .setUp("var = GlobalSymbol")
+        .testLookup("var", "foo");
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--incompatible_no_target_output_group=true")
+        .testIfErrorContains(errorMessage,
+            "var = GlobalSymbol");
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--incompatible_no_target_output_group=true")
+        .testIfErrorContains(errorMessage,
+            "def my_function():",
+            "  var = GlobalSymbol");
+
+    new SkylarkTest(
+            ImmutableMap.of("GlobalSymbol", val),
+            "--incompatible_no_target_output_group=true")
+        .setUp("GlobalSymbol = 'other'",
+            "var = GlobalSymbol")
+        .testLookup("var", "other");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
index 4f6c725..ac07655 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
@@ -17,6 +17,7 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.truth.Ordered;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventCollector;
@@ -39,6 +40,7 @@
 import com.google.devtools.build.lib.testutil.TestMode;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import org.junit.Before;
 
 /**
@@ -98,12 +100,17 @@
 
   protected Environment newEnvironmentWithSkylarkOptions(String... skylarkOptions)
       throws Exception {
+    return newEnvironmentWithBuiltinsAndSkylarkOptions(ImmutableMap.of(), skylarkOptions);
+  }
+
+  protected Environment newEnvironmentWithBuiltinsAndSkylarkOptions(Map<String, Object> builtins,
+      String... skylarkOptions) throws Exception {
     if (testMode == null) {
       throw new IllegalArgumentException(
           "TestMode is null. Please set a Testmode via setMode() or set the "
               + "Environment manually by overriding newEnvironment()");
     }
-    return testMode.createEnvironment(getEventHandler(), skylarkOptions);
+    return testMode.createEnvironment(getEventHandler(), builtins, skylarkOptions);
   }
 
   /**
@@ -117,6 +124,17 @@
     env = newEnvironmentWithSkylarkOptions(skylarkOptions);
   }
 
+  protected void setMode(TestMode testMode, Map<String, Object> builtins,
+      String... skylarkOptions) throws Exception {
+    this.testMode = testMode;
+    env = newEnvironmentWithBuiltinsAndSkylarkOptions(builtins, skylarkOptions);
+  }
+
+  protected void enableSkylarkMode(Map<String, Object> builtins,
+      String... skylarkOptions) throws Exception {
+    setMode(TestMode.SKYLARK, builtins, skylarkOptions);
+  }
+
   protected void enableSkylarkMode(String... skylarkOptions) throws Exception {
     setMode(TestMode.SKYLARK, skylarkOptions);
   }
@@ -589,14 +607,20 @@
    */
   protected class SkylarkTest extends ModalTestCase {
     private final String[] skylarkOptions;
+    private final Map<String, Object> builtins;
 
     public SkylarkTest(String... skylarkOptions) {
+      this(ImmutableMap.of(), skylarkOptions);
+    }
+
+    public SkylarkTest(Map<String, Object> builtins, String... skylarkOptions) {
+      this.builtins = builtins;
       this.skylarkOptions = skylarkOptions;
     }
 
     @Override
     protected void run(Testable testable) throws Exception {
-      enableSkylarkMode(skylarkOptions);
+      enableSkylarkMode(builtins, skylarkOptions);
       testable.run();
     }
   }
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java b/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java
index 9b848bd..e850ac6 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java
@@ -13,13 +13,16 @@
 // limitations under the License.
 package com.google.devtools.build.lib.testutil;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.skylark.SkylarkModules;
 import com.google.devtools.build.lib.events.EventHandler;
-import com.google.devtools.build.lib.packages.BazelLibrary;
 import com.google.devtools.build.lib.packages.SkylarkSemanticsOptions;
 import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Environment.GlobalFrame;
 import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.SkylarkSemantics;
 import com.google.devtools.common.options.OptionsParser;
+import java.util.Map;
 
 /**
  * Describes a particular testing mode by determining how the
@@ -36,10 +39,12 @@
   public static final TestMode BUILD =
       new TestMode() {
         @Override
-        public Environment createEnvironment(EventHandler eventHandler, String... skylarkOptions)
+        public Environment createEnvironment(EventHandler eventHandler,
+            Map<String, Object> builtins,
+            String... skylarkOptions)
             throws Exception {
           return Environment.builder(Mutability.create("build test"))
-              .setGlobals(BazelLibrary.GLOBALS)
+              .setGlobals(createGlobalFrame(builtins))
               .setEventHandler(eventHandler)
               .setSemantics(TestMode.parseSkylarkSemantics(skylarkOptions))
               .build();
@@ -49,16 +54,26 @@
   public static final TestMode SKYLARK =
       new TestMode() {
         @Override
-        public Environment createEnvironment(EventHandler eventHandler, String... skylarkOptions)
+        public Environment createEnvironment(EventHandler eventHandler,
+            Map<String, Object> builtins, String... skylarkOptions)
             throws Exception {
           return Environment.builder(Mutability.create("skylark test"))
-              .setGlobals(BazelLibrary.GLOBALS)
+              .setGlobals(createGlobalFrame(builtins))
               .setEventHandler(eventHandler)
               .setSemantics(TestMode.parseSkylarkSemantics(skylarkOptions))
               .build();
         }
       };
 
-  public abstract Environment createEnvironment(EventHandler eventHandler, String... skylarkOptions)
-      throws Exception;
+  private static GlobalFrame createGlobalFrame(Map<String, Object> builtins) {
+    ImmutableMap.Builder<String, Object> envBuilder = ImmutableMap.builder();
+
+    SkylarkModules.addSkylarkGlobalsToBuilder(envBuilder);
+    envBuilder.putAll(builtins);
+    return GlobalFrame.createForBuiltins(envBuilder.build());
+  }
+
+  public abstract Environment createEnvironment(EventHandler eventHandler,
+      Map<String, Object> builtins,
+      String... skylarkOptions) throws Exception;
 }