Introduce --incompatible_no_transitive_loads

With flag set, loaded symbols are not automatically re-exported.

#5636

RELNOTES: None.
PiperOrigin-RevId: 214776940
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkSemanticsOptions.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkSemanticsOptions.java
index 4236b12..07ec878 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkSemanticsOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkSemanticsOptions.java
@@ -319,6 +319,20 @@
   public boolean incompatibleNoSupportToolsInActionInputs;
 
   @Option(
+      name = "incompatible_no_transitive_loads",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.SKYLARK_SEMANTICS,
+      effectTags = {OptionEffectTag.BUILD_FILE_SEMANTICS},
+      metadataTags = {
+        OptionMetadataTag.INCOMPATIBLE_CHANGE,
+        OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
+      },
+      help =
+          "If set to true, only symbols explicitly defined in the file can be loaded; "
+              + "symbols introduced by load are not implicitly re-exported.")
+  public boolean incompatibleNoTransitiveLoads;
+
+  @Option(
       name = "incompatible_package_name_is_a_function",
       defaultValue = "false",
       documentationCategory = OptionDocumentationCategory.SKYLARK_SEMANTICS,
@@ -451,6 +465,7 @@
         .incompatibleGenerateJavaCommonSourceJar(incompatibleGenerateJavaCommonSourceJar)
         .incompatibleNewActionsApi(incompatibleNewActionsApi)
         .incompatibleNoSupportToolsInActionInputs(incompatibleNoSupportToolsInActionInputs)
+        .incompatibleNoTransitiveLoads(incompatibleNoTransitiveLoads)
         .incompatiblePackageNameIsAFunction(incompatiblePackageNameIsAFunction)
         .incompatibleRangeType(incompatibleRangeType)
         .incompatibleRemoveNativeGitRepository(incompatibleRemoveNativeGitRepository)
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 3a4f0c9..8e01e89 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
@@ -36,6 +36,7 @@
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -271,12 +272,16 @@
     /** Bindings are maintained in order of creation. */
     private final LinkedHashMap<String, Object> bindings;
 
+    /** Set of bindings that are exported (can be loaded from other modules). */
+    private final HashSet<String> exportedBindings;
+
     /** Constructs an uninitialized instance; caller must call {@link #initialize} before use. */
     public GlobalFrame() {
       this.mutability = null;
       this.universe = null;
       this.label = null;
       this.bindings = new LinkedHashMap<>();
+      this.exportedBindings = new HashSet<>();
     }
 
     public GlobalFrame(
@@ -298,6 +303,7 @@
       if (bindings != null) {
         this.bindings.putAll(bindings);
       }
+      this.exportedBindings = new HashSet<>();
     }
 
     public GlobalFrame(Mutability mutability) {
@@ -399,6 +405,21 @@
       return Collections.unmodifiableMap(bindings);
     }
 
+    /**
+     * Returns a map of bindings that are exported (i.e. symbols declared using `=` and
+     * `def`, but not `load`).
+     */
+    public Map<String, Object> getExportedBindings() {
+      checkInitialized();
+      ImmutableMap.Builder<String, Object> result = new ImmutableMap.Builder<>();
+      for (Map.Entry<String, Object> entry : bindings.entrySet()) {
+        if (exportedBindings.contains(entry.getKey())) {
+          result.put(entry);
+        }
+      }
+      return result.build();
+    }
+
     @Override
     public Map<String, Object> getTransitiveBindings() {
       checkInitialized();
@@ -523,7 +544,14 @@
      * and that {@code Environment}'s transitive hash code.
      */
     public Extension(Environment env) {
-      this(ImmutableMap.copyOf(env.globalFrame.bindings), env.getTransitiveContentHashCode());
+      // Legacy behavior: all symbols from the global Frame are exported (including symbols
+      // introduced by load).
+      this(
+          ImmutableMap.copyOf(
+              env.getSemantics().incompatibleNoTransitiveLoads()
+                  ? env.globalFrame.getExportedBindings()
+                  : env.globalFrame.getBindings()),
+          env.getTransitiveContentHashCode());
     }
 
     public String getTransitiveContentHashCode() {
@@ -981,6 +1009,15 @@
     }
   }
 
+  /** Modifies a binding in the current Frame. If it is the module Frame, also export it. */
+  public Environment updateAndExport(String varname, Object value) throws EvalException {
+    update(varname, value);
+    if (isGlobal()) {
+      globalFrame.exportedBindings.add(varname);
+    }
+    return this;
+  }
+
   /**
    * Modifies a binding in the current Frame of this Environment, as would an {@link
    * AssignmentStatement}. Does not try to modify an inherited binding. This will shadow any
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
index a91ffae..d7e30dd 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
@@ -121,7 +121,7 @@
       throw new EvalException(node.getLocation(), "Keyword-only argument is forbidden.");
     }
 
-    env.update(
+    env.updateAndExport(
         node.getIdentifier().getName(),
         new UserDefinedFunction(
             node.getIdentifier().getName(),
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/LValue.java b/src/main/java/com/google/devtools/build/lib/syntax/LValue.java
index 223e192..c37d032 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/LValue.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/LValue.java
@@ -89,7 +89,7 @@
   /** Binds a variable to the given value in the environment. */
   private static void assignIdentifier(Identifier ident, Object value, Environment env)
       throws EvalException {
-    env.update(ident.getName(), value);
+    env.updateAndExport(ident.getName(), value);
   }
 
   /**
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 686c21e..2d606cc 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
@@ -125,6 +125,8 @@
 
   public abstract boolean incompatibleNoSupportToolsInActionInputs();
 
+  public abstract boolean incompatibleNoTransitiveLoads();
+
   public abstract boolean incompatiblePackageNameIsAFunction();
 
   public abstract boolean incompatibleRangeType();
@@ -175,6 +177,7 @@
           .incompatibleGenerateJavaCommonSourceJar(false)
           .incompatibleNewActionsApi(false)
           .incompatibleNoSupportToolsInActionInputs(false)
+          .incompatibleNoTransitiveLoads(false)
           .incompatiblePackageNameIsAFunction(false)
           .incompatibleRangeType(false)
           .incompatibleRemoveNativeGitRepository(false)
@@ -228,6 +231,8 @@
 
     public abstract Builder incompatibleNoSupportToolsInActionInputs(boolean value);
 
+    public abstract Builder incompatibleNoTransitiveLoads(boolean value);
+
     public abstract Builder incompatiblePackageNameIsAFunction(boolean value);
 
     public abstract Builder incompatibleRangeType(boolean value);