Error-out if the label in a 'load' statement crosses a subpackage boundary. This behavior is guarded by a flag (default value is "don't error out"), and will eventually become the only behavior.

This implements the feature in #6408.

RELNOTES: None
PiperOrigin-RevId: 217402217
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/Label.java b/src/main/java/com/google/devtools/build/lib/cmdline/Label.java
index 43d9846..ecde524 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/Label.java
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/Label.java
@@ -639,6 +639,23 @@
     return label == null ? "(unknown)" : label.toString();
   }
 
+  /**
+   * Returns a {@link PathFragment} corresponding to the directory in which {@code label} would
+   * reside, if it were interpreted to be a path.
+   */
+  public static PathFragment getContainingDirectory(Label label) {
+    PathFragment pkg = label.getPackageFragment();
+    String name = label.getName();
+    if (name.equals(".")) {
+      return pkg;
+    }
+    if (PathFragment.isNormalizedRelativePath(name) && !PathFragment.containsSeparator(name)) {
+      // Optimize for the common case of a label like '//pkg:target'.
+      return pkg;
+    }
+    return pkg.getRelative(name).getParentDirectory();
+  }
+
   @Override
   public boolean isImmutable() {
     return true;
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 42da1f3..4af330c 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
@@ -265,6 +265,19 @@
   public boolean incompatibleDisallowLegacyJavaInfo;
 
   @Option(
+      name = "incompatible_disallow_load_labels_to_cross_package_boundaries",
+      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, the label argument to 'load' cannot cross a package boundary."
+  )
+  public boolean incompatibleDisallowLoadLabelsToCrossPackageBoundaries;
+
+  @Option(
       name = "incompatible_generate_javacommon_source_jar",
       defaultValue = "false",
       documentationCategory = OptionDocumentationCategory.SKYLARK_SEMANTICS,
@@ -520,6 +533,8 @@
         .incompatibleDisallowDictPlus(incompatibleDisallowDictPlus)
         .incompatibleDisallowFileType(incompatibleDisallowFileType)
         .incompatibleDisallowLegacyJavaInfo(incompatibleDisallowLegacyJavaInfo)
+        .incompatibleDisallowLoadLabelsToCrossPackageBoundaries(
+            incompatibleDisallowLoadLabelsToCrossPackageBoundaries)
         .incompatibleDisallowOldStyleArgsAdd(incompatibleDisallowOldStyleArgsAdd)
         .incompatibleDisallowSlashOperator(incompatibleDisallowSlashOperator)
         .incompatibleExpandDirectories(incompatibleExpandDirectories)
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
index bbf7511..93aa454 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
@@ -59,6 +59,9 @@
     PathFragment filePathFragment = fileLabel.toPathFragment();
 
     // Determine whether the package designated by fileLabel exists.
+    // TODO(bazel-team): After --incompatible_disallow_load_labels_to_cross_package_boundaries is
+    // removed and the new behavior is unconditional, we can instead safely assume the package
+    // exists and pass in the Root in the SkyKey and therefore this dep can be removed.
     SkyKey pkgSkyKey = PackageLookupValue.key(fileLabel.getPackageIdentifier());
     PackageLookupValue pkgLookupValue = null;
     try {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
index 725d201..c9a341e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
@@ -15,9 +15,11 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Interner;
+import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.concurrent.BlazeInterners;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.skyframe.AbstractSkyKey;
 import com.google.devtools.build.skyframe.SkyFunctionName;
@@ -47,6 +49,40 @@
     return Key.create(id);
   }
 
+  static String getErrorMessageForLabelCrossingPackageBoundary(
+      Root pkgRoot,
+      Label label,
+      ContainingPackageLookupValue containingPkgLookupValue) {
+    PackageIdentifier containingPkg = containingPkgLookupValue.getContainingPackageName();
+    boolean crossesPackageBoundaryBelow =
+        containingPkg.getSourceRoot().startsWith(label.getPackageIdentifier().getSourceRoot());
+    PathFragment labelNameFragment = PathFragment.create(label.getName());
+    String message = String.format("Label '%s' crosses boundary of %spackage '%s'",
+        label,
+        crossesPackageBoundaryBelow ? "sub" : "",
+        containingPkg);
+    Root containingRoot = containingPkgLookupValue.getContainingPackageRoot();
+    if (pkgRoot.equals(containingRoot)) {
+      PathFragment containingPkgFragment = containingPkg.getPackageFragment();
+      PathFragment labelNameInContainingPackage =
+          crossesPackageBoundaryBelow
+              ? labelNameFragment.subFragment(
+                  containingPkgFragment.segmentCount()
+                      - label.getPackageFragment().segmentCount(),
+                  labelNameFragment.segmentCount())
+              : label.toPathFragment().relativeTo(containingPkgFragment);
+      message += " (perhaps you meant to put the colon here: '";
+      if (containingPkg.getRepository().isDefault() || containingPkg.getRepository().isMain()) {
+        message += "//";
+      }
+      message += containingPkg + ":" + labelNameInContainingPackage + "'?)";
+    } else {
+      message += " (have you deleted " + containingPkg + "/BUILD? "
+          + "If so, use the --deleted_packages=" + containingPkg + " option)";
+    }
+    return message;
+  }
+
   @AutoCodec.VisibleForSerialization
   @AutoCodec
   static class Key extends AbstractSkyKey<PackageIdentifier> {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
index da528dc..4a0543f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -675,13 +675,7 @@
     Set<SkyKey> containingPkgLookupKeys = Sets.newHashSet();
     Map<Target, SkyKey> targetToKey = new HashMap<>();
     for (Target target : pkgBuilder.getTargets()) {
-      PathFragment dir = getContainingDirectory(target.getLabel());
-      if (dir == null) {
-        throw new IllegalStateException(
-            String.format(
-                "Null pkg for label %s as path fragment %s in pkg %s",
-                target.getLabel(), target.getLabel().getPackageFragment(), pkgId));
-      }
+      PathFragment dir = Label.getContainingDirectory(target.getLabel());
       if (dir.equals(pkgDir)) {
         continue;
       }
@@ -707,27 +701,18 @@
       ContainingPackageLookupValue containingPackageLookupValue =
           getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
               pkgId, containingPkgLookupValues.get(key), env);
-        if (maybeAddEventAboutLabelCrossingSubpackage(pkgBuilder, pkgRoot, target.getLabel(),
-          target.getLocation(), containingPackageLookupValue)) {
+      if (maybeAddEventAboutLabelCrossingSubpackage(
+          pkgBuilder,
+          pkgRoot,
+          target.getLabel(),
+          target.getLocation(),
+          containingPackageLookupValue)) {
         pkgBuilder.removeTarget(target);
         pkgBuilder.setContainsErrors();
       }
     }
   }
 
-  private static PathFragment getContainingDirectory(Label label) {
-    PathFragment pkg = label.getPackageFragment();
-    String name = label.getName();
-    if (name.equals(".")) {
-      return pkg;
-    }
-    if (PathFragment.isNormalizedRelativePath(name) && !PathFragment.containsSeparator(name)) {
-      // Optimize for the common case of a label like '//pkg:target'.
-      return pkg;
-    }
-    return pkg.getRelative(name).getParentDirectory();
-  }
-
   @Nullable
   private static ContainingPackageLookupValue
       getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
@@ -774,24 +759,8 @@
       // exceptions), it reaches here, and we tolerate it.
       return false;
     }
-    PathFragment labelNameFragment = PathFragment.create(label.getName());
-    String message = String.format("Label '%s' crosses boundary of subpackage '%s'",
-        label, containingPkg);
-    Root containingRoot = containingPkgLookupValue.getContainingPackageRoot();
-    if (pkgRoot.equals(containingRoot)) {
-      PathFragment labelNameInContainingPackage = labelNameFragment.subFragment(
-          containingPkg.getPackageFragment().segmentCount()
-              - label.getPackageFragment().segmentCount(),
-          labelNameFragment.segmentCount());
-      message += " (perhaps you meant to put the colon here: '";
-      if (containingPkg.getRepository().isDefault() || containingPkg.getRepository().isMain()) {
-        message += "//";
-      }
-      message += containingPkg + ":" + labelNameInContainingPackage + "'?)";
-    } else {
-      message += " (have you deleted " + containingPkg + "/BUILD? "
-          + "If so, use the --deleted_packages=" + containingPkg + " option)";
-    }
+    String message = ContainingPackageLookupValue.getErrorMessageForLabelCrossingPackageBoundary(
+        pkgRoot, label, containingPkgLookupValue);
     pkgBuilder.addEvent(Event.error(location, message));
     return true;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
index e16b5e9..dd1e50a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
@@ -207,6 +207,34 @@
       return null;
     }
 
+    if (skylarkSemantics.incompatibleDisallowLoadLabelsToCrossPackageBoundaries()) {
+      PathFragment dir = Label.getContainingDirectory(fileLabel);
+      PackageIdentifier dirId =
+          PackageIdentifier.create(fileLabel.getPackageIdentifier().getRepository(), dir);
+      ContainingPackageLookupValue containingPackageLookupValue;
+      try {
+        containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow(
+            ContainingPackageLookupValue.key(dirId),
+            BuildFileNotFoundException.class,
+            InconsistentFilesystemException.class);
+      } catch (BuildFileNotFoundException e) {
+        throw SkylarkImportFailedException.errorReadingFile(
+            fileLabel.toPathFragment(),
+            new ErrorReadingSkylarkExtensionException(e));
+      }
+      if (containingPackageLookupValue == null) {
+        return null;
+      }
+      if (!containingPackageLookupValue.hasContainingPackage()) {
+        throw SkylarkImportFailedException.noBuildFile(fileLabel.toPathFragment());
+      }
+      if (!containingPackageLookupValue.getContainingPackageName().equals(
+          fileLabel.getPackageIdentifier())) {
+        throw SkylarkImportFailedException.labelCrossesPackageBoundary(
+            fileLabel, containingPackageLookupValue);
+      }
+    }
+
     // Load the AST corresponding to this file.
     ASTFileLookupValue astLookupValue;
     try {
@@ -534,6 +562,20 @@
               + " Note that this BUILD file does not need to do anything except exist.", file));
     }
 
+    static SkylarkImportFailedException labelCrossesPackageBoundary(
+        Label fileLabel,
+        ContainingPackageLookupValue containingPackageLookupValue) {
+      return new SkylarkImportFailedException(
+          ContainingPackageLookupValue.getErrorMessageForLabelCrossingPackageBoundary(
+              // We don't actually know the proper Root to pass in here (since we don't e.g. know
+              // the root of the bzl/BUILD file that is trying to load 'fileLabel'). Therefore we
+              // just pass in the Root of the containing package in order to still get a useful
+              // error message for the user.
+              containingPackageLookupValue.getContainingPackageRoot(),
+              fileLabel,
+              containingPackageLookupValue));
+    }
+
     static SkylarkImportFailedException skylarkErrors(PathFragment file) {
       return new SkylarkImportFailedException(String.format("Extension '%s' has errors", file));
     }
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 d6eb28e..9fba528 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
@@ -124,6 +124,8 @@
 
   public abstract boolean incompatibleDisallowLegacyJavaInfo();
 
+  public abstract boolean incompatibleDisallowLoadLabelsToCrossPackageBoundaries();
+
   public abstract boolean incompatibleDisallowOldStyleArgsAdd();
 
   public abstract boolean incompatibleDisallowSlashOperator();
@@ -189,6 +191,7 @@
           .incompatibleDisallowDictPlus(false)
           .incompatibleDisallowFileType(false)
           .incompatibleDisallowLegacyJavaInfo(false)
+          .incompatibleDisallowLoadLabelsToCrossPackageBoundaries(false)
           .incompatibleDisallowOldStyleArgsAdd(false)
           .incompatibleDisallowSlashOperator(false)
           .incompatibleExpandDirectories(false)
@@ -245,6 +248,8 @@
 
     public abstract Builder incompatibleDisallowLegacyJavaInfo(boolean value);
 
+    public abstract Builder incompatibleDisallowLoadLabelsToCrossPackageBoundaries(boolean value);
+
     public abstract Builder incompatibleDisallowOldStyleArgsAdd(boolean value);
 
     public abstract Builder incompatibleDisallowSlashOperator(boolean value);