[7.4.0] Add `override_repo` and `inject_repo` (#23938)

Work towards https://github.com/bazelbuild/bazel/issues/19301
Fixes https://github.com/bazelbuild/bazel/issues/23580

RELNOTES: `override_repo` and `inject_repo` can be used to override and
inject repos in module extensions.

Closes https://github.com/bazelbuild/bazel/pull/23534.

PiperOrigin-RevId: 678139661
Change-Id: Iea7caca949c00e701f056c1037e273fee9740e93

(cherry picked from commit
https://github.com/bazelbuild/bazel/commit/46341b1a59c7a1cc8ca0dc604ad53b6de7fee653)

Fixes #23724
Fixes #23799

Also includes:
* Disallow importing injected repos
(8472c9df4d24fe5a54079fe302fe455bccf6930d)

---------

Co-authored-by: Xùdōng Yáng <wyverald@gmail.com>
diff --git a/site/en/external/extension.md b/site/en/external/extension.md
index 0d973ab..67ea7fd 100644
--- a/site/en/external/extension.md
+++ b/site/en/external/extension.md
@@ -176,6 +176,63 @@
         the repo visible to the module instead of the extension-generated repo
         of the same name.
 
+### Overriding and injecting module extension repos
+
+The root module can use
+[`override_repo`](/rules/lib/globals/module#override_repo) and
+[`inject_repo`](/rules/lib/globals/module#inject_repo) to override or inject
+module extension repos.
+
+#### Example: Replacing `rules_java`'s `java_tools` with a vendored copy
+
+```python
+# MODULE.bazel
+local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository")
+local_repository(
+  name = "my_java_tools",
+  path = "vendor/java_tools",
+)
+
+bazel_dep(name = "rules_java", version = "7.11.1")
+java_toolchains = use_extension("@rules_java//java:extension.bzl", "toolchains")
+
+override_repo(java_toolchains, remote_java_tools = "my_java_tools")
+```
+
+#### Example: Patch a Go dependency to depend on `@zlib` instead of the system zlib
+
+```python
+# MODULE.bazel
+bazel_dep(name = "gazelle", version = "0.38.0")
+bazel_dep(name = "zlib", version = "1.3.1.bcr.3")
+
+go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
+go_deps.from_file(go_mod = "//:go.mod")
+go_deps.module_override(
+  patches = [
+    "//patches:my_module_zlib.patch",
+  ],
+  path = "example.com/my_module",
+)
+use_repo(go_deps, ...)
+
+inject_repo(go_deps, "zlib")
+```
+
+```diff
+# patches/my_module_zlib.patch
+--- a/BUILD.bazel
++++ b/BUILD.bazel
+@@ -1,6 +1,6 @@
+ go_binary(
+     name = "my_module",
+     importpath = "example.com/my_module",
+     srcs = ["my_module.go"],
+-    copts = ["-lz"],
++    cdeps = ["@zlib"],
+ )
+```
+
 ## Best practices
 
 This section describes best practices when writing extensions so they are
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
index 7faf5a1..a04e21e 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
@@ -79,15 +79,24 @@
     ImmutableBiMap<String, ModuleExtensionId> extensionUniqueNames =
         calculateUniqueNameForUsedExtensionId(extensionUsagesById, starlarkSemantics);
 
+    char repoNameSeparator =
+        starlarkSemantics.getBool(BuildLanguageOptions.INCOMPATIBLE_USE_PLUS_IN_REPO_NAMES)
+            ? '+'
+            : '~';
+
     return BazelDepGraphValue.create(
         depGraph,
         canonicalRepoNameLookup,
         depGraph.values().stream().map(AbridgedModule::from).collect(toImmutableList()),
         extensionUsagesById,
         extensionUniqueNames.inverse(),
-        starlarkSemantics.getBool(BuildLanguageOptions.INCOMPATIBLE_USE_PLUS_IN_REPO_NAMES)
-            ? '+'
-            : '~');
+        resolveRepoOverrides(
+            depGraph,
+            extensionUsagesById,
+            extensionUniqueNames.inverse(),
+            canonicalRepoNameLookup,
+            repoNameSeparator),
+        repoNameSeparator);
   }
 
   private static ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage>
@@ -218,6 +227,40 @@
                 + extensionNameDisambiguator);
   }
 
+  private static ImmutableTable<ModuleExtensionId, String, RepositoryName> resolveRepoOverrides(
+      ImmutableMap<ModuleKey, Module> depGraph,
+      ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesTable,
+      ImmutableMap<ModuleExtensionId, String> extensionUniqueNames,
+      ImmutableBiMap<RepositoryName, ModuleKey> canonicalRepoNameLookup,
+      char repoNameSeparator) {
+    RepositoryMapping rootModuleMappingWithoutOverrides =
+        BazelDepGraphValue.getRepositoryMapping(
+            ModuleKey.ROOT,
+            depGraph,
+            extensionUsagesTable,
+            extensionUniqueNames,
+            canonicalRepoNameLookup,
+            // ModuleFileFunction ensures that repos that override other repos are not themselves
+            // overridden, so we can safely pass an empty table here instead of resolving chains
+            // of overrides.
+            ImmutableTable.of(),
+            repoNameSeparator);
+    ImmutableTable.Builder<ModuleExtensionId, String, RepositoryName> repoOverridesBuilder =
+        ImmutableTable.builder();
+    for (var extensionId : extensionUsagesTable.rowKeySet()) {
+      var rootUsage = extensionUsagesTable.row(extensionId).get(ModuleKey.ROOT);
+      if (rootUsage != null) {
+        for (var override : rootUsage.getRepoOverrides().entrySet()) {
+          repoOverridesBuilder.put(
+              extensionId,
+              override.getKey(),
+              rootModuleMappingWithoutOverrides.get(override.getValue().overridingRepoName()));
+        }
+      }
+    }
+    return repoOverridesBuilder.buildOrThrow();
+  }
+
   static class BazelDepGraphFunctionException extends SkyFunctionException {
     BazelDepGraphFunctionException(ExternalDepsException e, Transience transience) {
       super(e, transience);
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphValue.java
index 8621775..1015784 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphValue.java
@@ -43,6 +43,7 @@
       ImmutableList<AbridgedModule> abridgedModules,
       ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesTable,
       ImmutableMap<ModuleExtensionId, String> extensionUniqueNames,
+      ImmutableTable<ModuleExtensionId, String, RepositoryName> repoOverrides,
       char repoNameSeparator) {
     return new AutoValue_BazelDepGraphValue(
         depGraph,
@@ -50,6 +51,7 @@
         abridgedModules,
         extensionUsagesTable,
         extensionUniqueNames,
+        repoOverrides,
         repoNameSeparator);
   }
 
@@ -75,6 +77,7 @@
         ImmutableList.of(),
         ImmutableTable.of(),
         ImmutableMap.of(),
+        ImmutableTable.of(),
         '+');
   }
 
@@ -107,6 +110,12 @@
    */
   public abstract ImmutableMap<ModuleExtensionId, String> getExtensionUniqueNames();
 
+  /**
+   * For each module extension, a mapping from the name of the repo exported by the extension to the
+   * canonical name of the repo that should override it (if any).
+   */
+  public abstract ImmutableTable<ModuleExtensionId, String, RepositoryName> getRepoOverrides();
+
   /** The character to use to separate the different segments of a canonical repo name. */
   public abstract char getRepoNameSeparator();
 
@@ -115,22 +124,45 @@
    * module deps and module extensions.
    */
   public final RepositoryMapping getFullRepoMapping(ModuleKey key) {
+    return getRepositoryMapping(
+        key,
+        getDepGraph(),
+        getExtensionUsagesTable(),
+        getExtensionUniqueNames(),
+        getCanonicalRepoNameLookup(),
+        getRepoOverrides(),
+        getRepoNameSeparator());
+  }
+
+  static RepositoryMapping getRepositoryMapping(
+      ModuleKey key,
+      ImmutableMap<ModuleKey, Module> depGraph,
+      ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesTable,
+      ImmutableMap<ModuleExtensionId, String> extensionUniqueNames,
+      ImmutableBiMap<RepositoryName, ModuleKey> canonicalRepoNameLookup,
+      ImmutableTable<ModuleExtensionId, String, RepositoryName> repoOverrides,
+      char repoNameSeparator) {
     ImmutableMap.Builder<String, RepositoryName> mapping = ImmutableMap.builder();
     for (Map.Entry<ModuleExtensionId, ModuleExtensionUsage> extIdAndUsage :
-        getExtensionUsagesTable().column(key).entrySet()) {
+        extensionUsagesTable.column(key).entrySet()) {
       ModuleExtensionId extensionId = extIdAndUsage.getKey();
       ModuleExtensionUsage usage = extIdAndUsage.getValue();
-      String repoNamePrefix = getExtensionUniqueNames().get(extensionId) + getRepoNameSeparator();
+      String repoNamePrefix = extensionUniqueNames.get(extensionId) + repoNameSeparator;
       for (ModuleExtensionUsage.Proxy proxy : usage.getProxies()) {
         for (Map.Entry<String, String> entry : proxy.getImports().entrySet()) {
-          String canonicalRepoName = repoNamePrefix + entry.getValue();
-          mapping.put(entry.getKey(), RepositoryName.createUnvalidated(canonicalRepoName));
+          RepositoryName defaultCanonicalRepoName =
+              RepositoryName.createUnvalidated(repoNamePrefix + entry.getValue());
+          mapping.put(
+              entry.getKey(),
+              repoOverrides
+                  .row(extensionId)
+                  .getOrDefault(entry.getValue(), defaultCanonicalRepoName));
         }
       }
     }
-    return getDepGraph()
+    return depGraph
         .get(key)
-        .getRepoMappingWithBazelDepsOnly(getCanonicalRepoNameLookup().inverse())
+        .getRepoMappingWithBazelDepsOnly(canonicalRepoNameLookup.inverse())
         .withAdditionalMappings(mapping.buildOrThrow());
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionEvalStarlarkThreadContext.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionEvalStarlarkThreadContext.java
index 2509f78..4768ea4 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionEvalStarlarkThreadContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionEvalStarlarkThreadContext.java
@@ -66,6 +66,7 @@
   private final String repoPrefix;
   private final PackageIdentifier basePackageId;
   private final RepositoryMapping baseRepoMapping;
+  private final ImmutableMap<String, RepositoryName> repoOverrides;
   private final BlazeDirectories directories;
   private final ExtendedEventHandler eventHandler;
   private final Map<String, RepoRuleCall> deferredRepos = new LinkedHashMap<>();
@@ -75,12 +76,14 @@
       String repoPrefix,
       PackageIdentifier basePackageId,
       RepositoryMapping baseRepoMapping,
+      ImmutableMap<String, RepositoryName> repoOverrides,
       BlazeDirectories directories,
       ExtendedEventHandler eventHandler) {
     this.extensionId = extensionId;
     this.repoPrefix = repoPrefix;
     this.basePackageId = basePackageId;
     this.baseRepoMapping = baseRepoMapping;
+    this.repoOverrides = repoOverrides;
     this.directories = directories;
     this.eventHandler = eventHandler;
   }
@@ -127,13 +130,15 @@
     // Make it possible to refer to extension repos in the label attributes of another extension
     // repo. Wrapping a label in Label(...) ensures that it is evaluated with respect to the
     // containing module's repo mapping instead.
-    var extensionRepos =
+    ImmutableMap.Builder<String, RepositoryName> entries = ImmutableMap.builder();
+    entries.putAll(baseRepoMapping.entries());
+    entries.putAll(
         Maps.asMap(
             deferredRepos.keySet(),
-            apparentName -> RepositoryName.createUnvalidated(repoPrefix + apparentName));
+            apparentName -> RepositoryName.createUnvalidated(repoPrefix + apparentName)));
+    entries.putAll(repoOverrides);
     RepositoryMapping fullRepoMapping =
-        RepositoryMapping.create(extensionRepos, baseRepoMapping.ownerRepo())
-            .withAdditionalMappings(baseRepoMapping);
+        RepositoryMapping.create(entries.buildKeepingLast(), baseRepoMapping.ownerRepo());
     // LINT.ThenChange(//src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionRepoMappingEntriesFunction.java)
 
     ImmutableMap.Builder<String, RepoSpec> repoSpecs = ImmutableMap.builder();
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionRepoMappingEntriesFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionRepoMappingEntriesFunction.java
index 9253d38..8e400bc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionRepoMappingEntriesFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionRepoMappingEntriesFunction.java
@@ -74,6 +74,7 @@
     ImmutableMap.Builder<String, RepositoryName> entries = ImmutableMap.builder();
     entries.putAll(bazelDepGraphValue.getFullRepoMapping(moduleKey).entries());
     entries.putAll(extensionEvalValue.getCanonicalRepoNameToInternalNames().inverse());
+    entries.putAll(bazelDepGraphValue.getRepoOverrides().row(extensionId));
     return ModuleExtensionRepoMappingEntriesValue.create(entries.buildKeepingLast(), moduleKey);
     // LINT.ThenChange(//src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionEvalStarlarkThreadContext.java)
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java
index e5dc76f..f4fc116 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java
@@ -19,6 +19,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.ryanharter.auto.value.gson.GenerateTypeAdapter;
@@ -130,6 +131,24 @@
     return getProxies().stream().anyMatch(p -> !p.isDevDependency());
   }
 
+  /**
+   * Represents a repo that overrides another repo within the scope of the extension.
+   *
+   * @param overridingRepoName The apparent name of the overriding repo in the root module.
+   * @param mustExist Whether this override should apply to an existing repo.
+   * @param location The location of the {@code override_repo} or {@code inject_repo} call.
+   */
+  @GenerateTypeAdapter
+  public record RepoOverride(String overridingRepoName, boolean mustExist, Location location) {}
+
+  /**
+   * Contains information about overrides that apply to repos generated by this extension. Keyed by
+   * the extension-local repo name.
+   *
+   * <p>This is only non-empty for root module usages.
+   */
+  public abstract ImmutableMap<String, RepoOverride> getRepoOverrides();
+
   public abstract Builder toBuilder();
 
   public static Builder builder() {
@@ -152,6 +171,11 @@
         // Extension implementation functions do not see the imports, they are only validated
         // against the set of generated repos in a validation step that comes afterward.
         .setProxies(ImmutableList.of())
+        // Tracked in SingleExtensionUsagesValue instead, using canonical instead of apparent names.
+        // Whether this override must apply to an existing repo as well as its source location also
+        // don't influence the evaluation of the extension as they are checked in
+        // SingleExtensionFunction.
+        .setRepoOverrides(ImmutableMap.of())
         .build();
   }
 
@@ -185,6 +209,9 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
+    public abstract Builder setRepoOverrides(ImmutableMap<String, RepoOverride> repoOverrides);
+
     public abstract ModuleExtensionUsage build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
index fa8f303..7d1c9770 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
@@ -435,7 +435,7 @@
     try {
       module = moduleThreadContext.buildModule(/* registry= */ null);
     } catch (EvalException e) {
-      eventHandler.handle(Event.error(e.getMessageWithStack()));
+      eventHandler.handle(Event.error(e.getInnermostLocation(), e.getMessageWithStack()));
       throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for the root module");
     }
     for (ModuleExtensionUsage usage : module.getExtensionUsages()) {
@@ -521,7 +521,7 @@
           });
       compiledRootModuleFile.runOnThread(thread);
     } catch (EvalException e) {
-      eventHandler.handle(Event.error(e.getMessageWithStack()));
+      eventHandler.handle(Event.error(e.getInnermostLocation(), e.getMessageWithStack()));
       throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for %s", moduleKey);
     }
     return context;
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
index b3371ae..028e4cb 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
@@ -15,7 +15,6 @@
 
 package com.google.devtools.build.lib.bazel.bzlmod;
 
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
@@ -49,7 +48,6 @@
 import net.starlark.java.eval.StarlarkValue;
 import net.starlark.java.eval.Structure;
 import net.starlark.java.eval.Tuple;
-import net.starlark.java.syntax.Location;
 
 /** A collection of global Starlark build API functions that apply to MODULE.bazel files. */
 @GlobalMethods(environment = Environment.MODULE)
@@ -171,11 +169,10 @@
     }
     if (repoName.isEmpty()) {
       repoName = name;
-      context.addRepoNameUsage(name, "as the current module name", thread.getCallerLocation());
+      context.addRepoNameUsage(name, "as the current module name", thread.getCallStack());
     } else {
       RepositoryName.validateUserProvidedRepoName(repoName);
-      context.addRepoNameUsage(
-          repoName, "as the module's own repo name", thread.getCallerLocation());
+      context.addRepoNameUsage(repoName, "as the module's own repo name", thread.getCallStack());
     }
     Version parsedVersion;
     try {
@@ -293,7 +290,7 @@
               name, parsedVersion, maxCompatibilityLevel.toInt("max_compatibility_level")));
     }
 
-    context.addRepoNameUsage(repoName, "by a bazel_dep", thread.getCallerLocation());
+    context.addRepoNameUsage(repoName, "by a bazel_dep", thread.getCallStack());
   }
 
   @StarlarkMethod(
@@ -525,12 +522,25 @@
       usageBuilder.addProxyBuilder(proxyBuilder);
     }
 
-    void addImport(String localRepoName, String exportedName, String byWhat, Location location)
+    void addImport(
+        String localRepoName,
+        String exportedName,
+        String byWhat,
+        ImmutableList<StarlarkThread.CallStackEntry> stack)
         throws EvalException {
-      usageBuilder.addImport(localRepoName, exportedName, byWhat, location);
+      usageBuilder.addImport(localRepoName, exportedName, byWhat, stack);
       proxyBuilder.addImport(localRepoName, exportedName);
     }
 
+    void addOverride(
+        String overriddenRepoName,
+        String overridingRepoName,
+        boolean mustExist,
+        ImmutableList<StarlarkThread.CallStackEntry> stack)
+        throws EvalException {
+      usageBuilder.addRepoOverride(overriddenRepoName, overridingRepoName, mustExist, stack);
+    }
+
     class TagCallable implements StarlarkValue {
       final String tagName;
 
@@ -610,13 +620,120 @@
       throws EvalException {
     ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "use_repo()");
     context.setNonModuleCalled();
-    Location location = thread.getCallerLocation();
+    ImmutableList<StarlarkThread.CallStackEntry> stack = thread.getCallStack();
     for (String arg : Sequence.cast(args, String.class, "args")) {
-      extensionProxy.addImport(arg, arg, "by a use_repo() call", location);
+      extensionProxy.addImport(arg, arg, "by a use_repo() call", stack);
     }
     for (Map.Entry<String, String> entry :
         Dict.cast(kwargs, String.class, String.class, "kwargs").entrySet()) {
-      extensionProxy.addImport(entry.getKey(), entry.getValue(), "by a use_repo() call", location);
+      extensionProxy.addImport(entry.getKey(), entry.getValue(), "by a use_repo() call", stack);
+    }
+  }
+
+  @StarlarkMethod(
+      name = "override_repo",
+      doc =
+          """
+          Overrides one or more repos defined by the given module extension with the given repos
+          visible to the current module. This is ignored if the current module is not the root
+          module or `--ignore_dev_dependency` is enabled.
+
+          <p>Use <a href="#inject_repo"><code>inject_repo</code></a> instead to add a new repo.
+          """,
+      parameters = {
+        @Param(
+            name = "extension_proxy",
+            doc = "A module extension proxy object returned by a <code>use_extension</code> call."),
+      },
+      extraPositionals =
+          @Param(
+              name = "args",
+              doc =
+                  """
+                  The repos in the extension that should be overridden with the repos of the same
+                  name in the current module."""),
+      extraKeywords =
+          @Param(
+              name = "kwargs",
+              doc =
+                  """
+                  The overrides to apply to the repos generated by the extension, where the values
+                  are the names of repos in the scope of the current module and the keys are the
+                  names of the repos they will override in the extension."""),
+      useStarlarkThread = true)
+  public void overrideRepo(
+      ModuleExtensionProxy extensionProxy,
+      Tuple args,
+      Dict<String, Object> kwargs,
+      StarlarkThread thread)
+      throws EvalException {
+    ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "override_repo()");
+    context.setNonModuleCalled();
+    if (context.shouldIgnoreDevDeps()) {
+      // Ignore calls early as they may refer to repos that are dev dependencies (or this is not the
+      // root module).
+      return;
+    }
+    ImmutableList<StarlarkThread.CallStackEntry> stack = thread.getCallStack();
+    for (String arg : Sequence.cast(args, String.class, "args")) {
+      extensionProxy.addOverride(arg, arg, /* mustExist= */ true, stack);
+    }
+    for (Map.Entry<String, String> entry :
+        Dict.cast(kwargs, String.class, String.class, "kwargs").entrySet()) {
+      extensionProxy.addOverride(entry.getKey(), entry.getValue(), /* mustExist= */ true, stack);
+    }
+  }
+
+  @StarlarkMethod(
+      name = "inject_repo",
+      doc =
+          """
+          Injects one or more new repos into the given module extension.
+          This is ignored if the current module is not the root module or `--ignore_dev_dependency`
+          is enabled.
+          <p>Use <a href="#override_repo"><code>override_repo</code></a> instead to override an
+          existing repo.""",
+      parameters = {
+        @Param(
+            name = "extension_proxy",
+            doc = "A module extension proxy object returned by a <code>use_extension</code> call."),
+      },
+      extraPositionals =
+          @Param(
+              name = "args",
+              doc =
+                  """
+                  The repos visible to the current module that should be injected into the
+                  extension under the same name."""),
+      extraKeywords =
+          @Param(
+              name = "kwargs",
+              doc =
+                  """
+                  The new repos to inject into the extension, where the values are the names of
+                  repos in the scope of the current module and the keys are the name they will be
+                  visible under in the extension."""),
+      useStarlarkThread = true)
+  public void injectRepo(
+      ModuleExtensionProxy extensionProxy,
+      Tuple args,
+      Dict<String, Object> kwargs,
+      StarlarkThread thread)
+      throws EvalException {
+    ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "inject_repo()");
+    context.setNonModuleCalled();
+    if (context.shouldIgnoreDevDeps()) {
+      // Ignore calls early as they may refer to repos that are dev dependencies (or this is not the
+      // root module).
+      return;
+    }
+    ImmutableList<StarlarkThread.CallStackEntry> stack = thread.getCallStack();
+    for (String arg : Sequence.cast(args, String.class, "args")) {
+      extensionProxy.addOverride(arg, arg, /* mustExist= */ false, stack);
+    }
+    for (Map.Entry<String, String> entry :
+        Dict.cast(kwargs, String.class, String.class, "kwargs").entrySet()) {
+      extensionProxy.addOverride(entry.getKey(), entry.getValue(), /* mustExist= */ false, stack);
     }
   }
 
@@ -697,7 +814,7 @@
                   .setContainingModuleFilePath(
                       usageBuilder.getContext().getCurrentModuleFilePath()));
       extensionProxy.getValue(tagName).call(kwargs, thread);
-      extensionProxy.addImport(name, name, "by a repo rule", thread.getCallerLocation());
+      extensionProxy.addImport(name, name, "by a repo rule", thread.getCallStack());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java
index cdfeb66..06231cc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.bazel.bzlmod.InterimModule.DepSpec;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
@@ -53,6 +54,9 @@
   private final Map<String, ModuleOverride> overrides = new LinkedHashMap<>();
   private final Map<String, RepoNameUsage> repoNameUsages = new HashMap<>();
 
+  private final Map<String, RepoOverride> overriddenRepos = new HashMap<>();
+  private final Map<String, RepoOverride> overridingRepos = new HashMap<>();
+
   public static ModuleThreadContext fromOrFail(StarlarkThread thread, String what)
       throws EvalException {
     ModuleThreadContext context = thread.getThreadLocal(ModuleThreadContext.class);
@@ -77,14 +81,33 @@
     this.includeLabelToCompiledModuleFile = includeLabelToCompiledModuleFile;
   }
 
-  record RepoNameUsage(String how, Location where) {}
+  record RepoOverride(
+      String overriddenRepoName,
+      String overridingRepoName,
+      boolean mustExist,
+      String extensionName,
+      ImmutableList<StarlarkThread.CallStackEntry> stack) {
+    Location location() {
+      // Skip over the override_repo builtin frame.
+      return stack.reverse().get(1).location;
+    }
+  }
 
-  public void addRepoNameUsage(String repoName, String how, Location where) throws EvalException {
-    RepoNameUsage collision = repoNameUsages.put(repoName, new RepoNameUsage(how, where));
+  record RepoNameUsage(String how, ImmutableList<StarlarkThread.CallStackEntry> stack) {
+    Location location() {
+      // Skip over the override_repo builtin frame.
+      return stack.reverse().get(1).location;
+    }
+  }
+
+  public void addRepoNameUsage(
+      String repoName, String how, ImmutableList<StarlarkThread.CallStackEntry> stack)
+      throws EvalException {
+    RepoNameUsage collision = repoNameUsages.put(repoName, new RepoNameUsage(how, stack));
     if (collision != null) {
       throw Starlark.errorf(
           "The repo name '%s' is already being used %s at %s",
-          repoName, collision.how(), collision.where());
+          repoName, collision.how(), collision.location());
     }
   }
 
@@ -123,12 +146,14 @@
   }
 
   static class ModuleExtensionUsageBuilder {
+
     private final ModuleThreadContext context;
     private final String extensionBzlFile;
     private final String extensionName;
     private final boolean isolate;
     private final ArrayList<ModuleExtensionUsage.Proxy.Builder> proxyBuilders;
     private final HashBiMap<String, String> imports;
+    private final Map<String, RepoOverride> repoOverrides;
     private final ImmutableList.Builder<Tag> tags;
 
     ModuleExtensionUsageBuilder(
@@ -142,6 +167,7 @@
       this.isolate = isolate;
       this.proxyBuilders = new ArrayList<>();
       this.imports = HashBiMap.create();
+      this.repoOverrides = new HashMap<>();
       this.tags = ImmutableList.builder();
     }
 
@@ -163,20 +189,41 @@
         String localRepoName,
         String exportedName,
         String byWhat,
-        Location location)
+        ImmutableList<StarlarkThread.CallStackEntry> stack)
         throws EvalException {
       RepositoryName.validateUserProvidedRepoName(localRepoName);
       RepositoryName.validateUserProvidedRepoName(exportedName);
-      context.addRepoNameUsage(localRepoName, byWhat, location);
+      context.addRepoNameUsage(localRepoName, byWhat, stack);
       if (imports.containsValue(exportedName)) {
         String collisionRepoName = imports.inverse().get(exportedName);
         throw Starlark.errorf(
             "The repo exported as '%s' by module extension '%s' is already imported at %s",
-            exportedName, extensionName, context.repoNameUsages.get(collisionRepoName).where());
+            exportedName, extensionName, context.repoNameUsages.get(collisionRepoName).location());
       }
       imports.put(localRepoName, exportedName);
     }
 
+    public void addRepoOverride(
+        String overriddenRepoName,
+        String overridingRepoName,
+        boolean mustExist,
+        ImmutableList<StarlarkThread.CallStackEntry> stack)
+        throws EvalException {
+      RepositoryName.validateUserProvidedRepoName(overriddenRepoName);
+      RepositoryName.validateUserProvidedRepoName(overridingRepoName);
+      RepoOverride collision =
+          repoOverrides.put(
+              overriddenRepoName,
+              new RepoOverride(
+                  overriddenRepoName, overridingRepoName, mustExist, extensionName, stack));
+      if (collision != null) {
+        throw Starlark.errorf(
+            "The repo exported as '%s' by module extension '%s' is already overridden with '%s' at"
+                + " %s",
+            overriddenRepoName, extensionName, collision.overridingRepoName, collision.location());
+      }
+    }
+
     void addTag(Tag tag) {
       tags.add(tag);
     }
@@ -203,6 +250,41 @@
       } else {
         builder.setIsolationKey(Optional.empty());
       }
+
+      for (var override : repoOverrides.entrySet()) {
+        String overriddenRepoName = override.getKey();
+        String overridingRepoName = override.getValue().overridingRepoName;
+        if (!context.repoNameUsages.containsKey(overridingRepoName)) {
+          throw Starlark.errorf(
+                  "The repo exported as '%s' by module extension '%s' is overridden with '%s', but"
+                      + " no repo is visible under this name",
+                  overriddenRepoName, extensionName, overridingRepoName)
+              .withCallStack(override.getValue().stack);
+        }
+        String importedAs = imports.inverse().get(overriddenRepoName);
+        if (importedAs != null) {
+          if (!override.getValue().mustExist) {
+            throw Starlark.errorf(
+                    "Cannot import repo '%s' that has been injected into module extension '%s' at"
+                        + " %s. Please refer to @%s directly.",
+                    overriddenRepoName,
+                    extensionName,
+                    override.getValue().location(),
+                    overridingRepoName)
+                .withCallStack(context.repoNameUsages.get(importedAs).stack);
+          }
+          context.overriddenRepos.put(importedAs, override.getValue());
+        }
+        context.overridingRepos.put(overridingRepoName, override.getValue());
+      }
+      builder.setRepoOverrides(
+          ImmutableMap.copyOf(
+              Maps.transformValues(
+                  repoOverrides,
+                  v ->
+                      new ModuleExtensionUsage.RepoOverride(
+                          v.overridingRepoName, v.mustExist, v.location()))));
+
       return builder.build();
     }
   }
@@ -249,7 +331,7 @@
       }
       deps.put(builtinModule, DepSpec.create(builtinModule, Version.EMPTY, -1));
       try {
-        addRepoNameUsage(builtinModule, "as a built-in dependency", Location.BUILTIN);
+        addRepoNameUsage(builtinModule, "as a built-in dependency", ImmutableList.of());
       } catch (EvalException e) {
         throw new EvalException(
             e.getMessage()
@@ -269,6 +351,24 @@
       }
       extensionUsages.add(extensionUsageBuilder.buildUsage());
     }
+    // A repo cannot be both overriding and overridden. This ensures that repo overrides can be
+    // applied to repo mappings in a single step (and also prevents cycles).
+    Optional<String> overridingAndOverridden =
+        overridingRepos.keySet().stream().filter(overriddenRepos::containsKey).findFirst();
+    if (overridingAndOverridden.isPresent()) {
+      var override = overridingRepos.get(overridingAndOverridden.get());
+      var overrideOnOverride = overriddenRepos.get(overridingAndOverridden.get());
+      throw Starlark.errorf(
+              "The repo '%s' used as an override for '%s' in module extension '%s' is itself"
+                  + " overridden with '%s' at %s, which is not supported.",
+              override.overridingRepoName,
+              override.overriddenRepoName,
+              override.extensionName,
+              overrideOnOverride.overridingRepoName,
+              overrideOnOverride.location())
+          .withCallStack(override.stack);
+    }
+
     return module
         .setRegistry(registry)
         .setDeps(ImmutableMap.copyOf(deps))
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegularRunnableExtension.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegularRunnableExtension.java
index 71a3a44..be4672a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegularRunnableExtension.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegularRunnableExtension.java
@@ -263,6 +263,7 @@
             usagesValue.getExtensionUniqueName() + separator,
             extensionId.getBzlFileLabel().getPackageIdentifier(),
             BazelModuleContext.of(bzlLoadValue.getModule()).repoMapping(),
+            usagesValue.getRepoOverrides(),
             directories,
             env.getListener());
     Optional<ModuleExtensionMetadata> moduleExtensionMetadata;
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionFunction.java
index d0a8e98..fbce3df 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionFunction.java
@@ -52,7 +52,8 @@
     for (ModuleExtensionUsage usage : usagesValue.getExtensionUsages().values()) {
       for (ModuleExtensionUsage.Proxy proxy : usage.getProxies()) {
         for (Entry<String, String> repoImport : proxy.getImports().entrySet()) {
-          if (!evalOnlyValue.getGeneratedRepoSpecs().containsKey(repoImport.getValue())) {
+          if (!evalOnlyValue.getGeneratedRepoSpecs().containsKey(repoImport.getValue())
+              && !usagesValue.getRepoOverrides().containsKey(repoImport.getValue())) {
             throw new SingleExtensionFunctionException(
                 ExternalDepsException.withMessage(
                     Code.INVALID_EXTENSION_IMPORT,
@@ -71,6 +72,38 @@
       }
     }
 
+    // Check that repo overrides apply as declared.
+    for (ModuleExtensionUsage usage : usagesValue.getExtensionUsages().values()) {
+      for (var override : usage.getRepoOverrides().entrySet()) {
+        boolean repoExists = evalOnlyValue.getGeneratedRepoSpecs().containsKey(override.getKey());
+        if (repoExists && !override.getValue().mustExist()) {
+          throw new SingleExtensionFunctionException(
+              ExternalDepsException.withMessage(
+                  Code.INVALID_EXTENSION_IMPORT,
+                  "module extension \"%s\" from \"%s\" generates repository \"%s\", yet"
+                      + " it is injected via inject_repo() at %s. Use override_repo() instead to"
+                      + " override an existing repository.",
+                  extensionId.getExtensionName(),
+                  extensionId.getBzlFileLabel(),
+                  override.getKey(),
+                  override.getValue().location()),
+              Transience.PERSISTENT);
+        } else if (!repoExists && override.getValue().mustExist()) {
+          throw new SingleExtensionFunctionException(
+              ExternalDepsException.withMessage(
+                  Code.INVALID_EXTENSION_IMPORT,
+                  "module extension \"%s\" from \"%s\" does not generate repository \"%s\", yet"
+                      + " it is overridden via override_repo() at %s. Use inject_repo() instead to"
+                      + " inject a new repository.",
+                  extensionId.getExtensionName(),
+                  extensionId.getBzlFileLabel(),
+                  override.getKey(),
+                  override.getValue().location()),
+              Transience.PERSISTENT);
+        }
+      }
+    }
+
     return evalOnlyValue;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesFunction.java
index 870eb6c..4a3ca17 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesFunction.java
@@ -61,6 +61,7 @@
             .collect(toImmutableList()),
         // TODO(wyv): Maybe cache these mappings?
         usagesTable.row(id).keySet().stream()
-            .collect(toImmutableMap(key -> key, bazelDepGraphValue::getFullRepoMapping)));
+            .collect(toImmutableMap(key -> key, bazelDepGraphValue::getFullRepoMapping)),
+        bazelDepGraphValue.getRepoOverrides().row(id));
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesValue.java
index fef7930..69a5c13 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionUsagesValue.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.hash.Hashing;
 import com.google.devtools.build.lib.cmdline.RepositoryMapping;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.skyframe.AbstractSkyKey;
@@ -55,13 +56,17 @@
   /** The repo mappings to use for each module that used this extension. */
   public abstract ImmutableMap<ModuleKey, RepositoryMapping> getRepoMappings();
 
+  /** Maps an extension-local repo name to the canonical name of the repo it is overridden with. */
+  public abstract ImmutableMap<String, RepositoryName> getRepoOverrides();
+
   public static SingleExtensionUsagesValue create(
       ImmutableMap<ModuleKey, ModuleExtensionUsage> extensionUsages,
       String extensionUniqueName,
       ImmutableList<AbridgedModule> abridgedModules,
-      ImmutableMap<ModuleKey, RepositoryMapping> repoMappings) {
+      ImmutableMap<ModuleKey, RepositoryMapping> repoMappings,
+      ImmutableMap<String, RepositoryName> repoOverrides) {
     return new AutoValue_SingleExtensionUsagesValue(
-        extensionUsages, extensionUniqueName, abridgedModules, repoMappings);
+        extensionUsages, extensionUniqueName, abridgedModules, repoMappings, repoOverrides);
   }
 
   /**
@@ -88,7 +93,8 @@
         // repoMappings: The usage of repo mappings by the extension's implementation function is
         // tracked on the level of individual entries and all label attributes are provided as
         // `Label`, which exclusively reference canonical repository names.
-        ImmutableMap.of());
+        ImmutableMap.of(),
+        getRepoOverrides());
   }
 
   public static Key key(ModuleExtensionId id) {
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java
index 19eae46..46b4845 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java
@@ -220,6 +220,7 @@
         .setExtensionBzlFile(bzlFile)
         .setExtensionName(name)
         .setIsolationKey(Optional.empty())
+        .setRepoOverrides(ImmutableMap.of())
         .addProxy(
             ModuleExtensionUsage.Proxy.builder()
                 .setDevDependency(false)
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java
index 3c36c8e..45d7683 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java
@@ -1451,6 +1451,7 @@
         """
         ERROR /ws/defs.bzl:9:12: Traceback (most recent call last):
         \tFile "/ws/defs.bzl", line 9, column 12, in _ext_impl
+        \t\tdata_repo(name='ext',data='@not_other_repo//:foo')
         Error in repository_rule: no repository visible as '@not_other_repo' in \
         the extension '@@//:defs.bzl%ext', but referenced by label \
         '@not_other_repo//:foo' in attribute 'data' of data_repo 'ext'.""");
@@ -1495,6 +1496,7 @@
         Traceback (most recent call last):
         \tFile "/usr/local/google/_blaze_jrluser/FAKEMD5/external/ext_module~/defs.bzl", \
         line 9, column 12, in _ext_impl
+        \t\tdata_repo(name='ext',data='@not_other_repo//:foo')
         Error in repository_rule: no repository visible as '@not_other_repo' in the extension \
         '@@ext_module~//:defs.bzl%ext', but referenced by label '@not_other_repo//:foo' in \
         attribute 'data' of data_repo 'ext'.""");
@@ -1573,6 +1575,7 @@
         """
         ERROR /ws/defs.bzl:9:12: Traceback (most recent call last):
         \tFile "/ws/defs.bzl", line 9, column 12, in _ext_impl
+        \t\tdata_repo(name='ext',data=['@not_other_repo//:foo'])
         Error in repository_rule: no repository visible as '@not_other_repo' \
         in the extension '@@//:defs.bzl%ext', but referenced by label \
         '@not_other_repo//:foo' in attribute 'data' of data_repo 'ext'.""");
@@ -1609,6 +1612,7 @@
         """
         ERROR /ws/defs.bzl:9:12: Traceback (most recent call last):
         \tFile "/ws/defs.bzl", line 9, column 12, in _ext_impl
+        \t\tdata_repo(name='ext',data={'@not_other_repo//:foo':'bar'})
         Error in repository_rule: no repository visible as '@not_other_repo' \
         in the extension '@@//:defs.bzl%ext', but referenced by label \
         '@not_other_repo//:foo' in attribute 'data' of data_repo 'ext'.""");
@@ -2065,6 +2069,7 @@
         "  'dev_as_non_dev_dep',",
         "  my_direct_dep = 'direct_dep',",
         ")",
+        "inject_repo(ext, my_data_repo = 'data_repo')",
         "ext_dev = use_extension('@ext//:defs.bzl', 'ext', dev_dependency = True)",
         "use_repo(",
         "  ext_dev,",
@@ -3120,4 +3125,211 @@
             "@@_main~other_ext~other_bar//:bar")
         .inOrder();
   }
+
+  @Test
+  public void overrideRepo_override() throws Exception {
+    scratch.file(
+        workspaceRoot.getRelative("MODULE.bazel").getPathString(),
+        """
+        bazel_dep(name = "data_repo", version = "1.0")
+        ext = use_extension("//:defs.bzl","ext")
+        use_repo(ext, "bar", module_foo = "foo")
+        data_repo = use_repo_rule("@data_repo//:defs.bzl", "data_repo")
+        data_repo(name = "override", data = "overridden_data")
+        override_repo(ext, foo = "override")
+        """);
+    scratch.file(workspaceRoot.getRelative("BUILD").getPathString());
+    scratch.file(
+        workspaceRoot.getRelative("data.bzl").getPathString(),
+        """
+        load("@bar//:list.bzl", _bar_list = "list")
+        load("@override//:data.bzl", _override_data = "data")
+        load("@module_foo//:data.bzl", _foo_data = "data")
+        bar_list = _bar_list
+        foo_data = _foo_data
+        override_data = _override_data
+        """);
+    scratch.file(
+        workspaceRoot.getRelative("defs.bzl").getPathString(),
+        """
+        load("@data_repo//:defs.bzl", "data_repo")
+        def _list_repo_impl(ctx):
+          ctx.file("WORKSPACE")
+          ctx.file("BUILD")
+          labels = [str(Label(l)) for l in ctx.attr.labels]
+          labels += [str(Label("@module_foo//:target3"))]
+          ctx.file("list.bzl", "list = " + repr(labels) + " + [str(Label('@foo//:target4'))]")
+        list_repo = repository_rule(
+          implementation = _list_repo_impl,
+          attrs = {
+            "labels": attr.label_list(),
+          },
+        )
+        def _fail_repo_impl(ctx):
+          fail("This rule should not be evaluated")
+        fail_repo = repository_rule(implementation = _fail_repo_impl)
+        def _ext_impl(ctx):
+          fail_repo(name = "foo")
+          list_repo(
+            name = "bar",
+            labels = [
+              # lazy extension implementation function repository mapping
+              "@foo//:target1",
+              # module repo repository mapping
+              "@module_foo//:target2",
+            ],
+          )
+        ext = module_extension(implementation = _ext_impl)
+        """);
+
+    SkyKey skyKey = BzlLoadValue.keyForBuild(Label.parseCanonical("//:data.bzl"));
+    EvaluationResult<BzlLoadValue> result =
+        evaluator.evaluate(ImmutableList.of(skyKey), evaluationContext);
+    if (result.hasError()) {
+      throw result.getError().getException();
+    }
+    assertThat((List<?>) result.get(skyKey).getModule().getGlobal("bar_list"))
+        .containsExactly(
+            "@@_main~_repo_rules~override//:target1",
+            "@@_main~_repo_rules~override//:target2",
+            "@@_main~_repo_rules~override//:target3",
+            "@@_main~_repo_rules~override//:target4")
+        .inOrder();
+    Object overrideData = result.get(skyKey).getModule().getGlobal("override_data");
+    assertThat(overrideData).isInstanceOf(String.class);
+    assertThat(overrideData).isEqualTo("overridden_data");
+    Object fooData = result.get(skyKey).getModule().getGlobal("foo_data");
+    assertThat(fooData).isSameInstanceAs(overrideData);
+  }
+
+  @Test
+  public void overrideRepo_override_onNonExistentRepoFails() throws Exception {
+    scratch.file(
+        workspaceRoot.getRelative("MODULE.bazel").getPathString(),
+        """
+        bazel_dep(name = "data_repo", version = "1.0")
+        ext = use_extension("//:defs.bzl","ext")
+        use_repo(ext, "bar", module_foo = "foo")
+        data_repo = use_repo_rule("@data_repo//:defs.bzl", "data_repo")
+        data_repo(name = "foo", data = "overridden_data")
+        override_repo(ext, "foo")
+        """);
+    scratch.file(workspaceRoot.getRelative("BUILD").getPathString());
+    scratch.file(
+        workspaceRoot.getRelative("data.bzl").getPathString(),
+        """
+        load("@bar//:list.bzl", _bar_list = "list")
+        load("@foo//:data.bzl", _foo_data = "data")
+        bar_list = _bar_list
+        foo_data = _foo_data
+        """);
+    scratch.file(
+        workspaceRoot.getRelative("defs.bzl").getPathString(),
+        """
+        load("@data_repo//:defs.bzl", "data_repo")
+        def _list_repo_impl(ctx):
+          ctx.file("WORKSPACE")
+          ctx.file("BUILD")
+          labels = [str(Label(l)) for l in ctx.attr.labels]
+          labels += [str(Label("@foo//:target3"))]
+          ctx.file("list.bzl", "list = " + repr(labels) + " + [str(Label('@foo//:target4'))]")
+        list_repo = repository_rule(
+          implementation = _list_repo_impl,
+          attrs = {
+            "labels": attr.label_list(),
+          },
+        )
+        def _ext_impl(ctx):
+          list_repo(
+            name = "bar",
+            labels = [
+              # lazy extension implementation function repository mapping
+              "@foo//:target1",
+              # module repo repository mapping
+              Label("@foo//:target2"),
+            ],
+          )
+        ext = module_extension(implementation = _ext_impl)
+        """);
+
+    SkyKey skyKey = BzlLoadValue.keyForBuild(Label.parseCanonical("//:data.bzl"));
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<BzlLoadValue> result =
+        evaluator.evaluate(ImmutableList.of(skyKey), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+    assertThat(result.getError().getException())
+        .hasMessageThat()
+        .isEqualTo(
+            "module extension \"ext\" from \"//:defs.bzl\" does not generate repository \"foo\","
+                + " yet it is overridden via override_repo() at /ws/MODULE.bazel:6:14. Use"
+                + " inject_repo() instead to inject a new repository.");
+  }
+
+  @Test
+  public void overrideRepo_inject() throws Exception {
+    scratch.file(
+        workspaceRoot.getRelative("MODULE.bazel").getPathString(),
+        """
+        bazel_dep(name = "data_repo", version = "1.0")
+        ext = use_extension("//:defs.bzl","ext")
+        use_repo(ext, "bar")
+        data_repo = use_repo_rule("@data_repo//:defs.bzl", "data_repo")
+        data_repo(name = "foo", data = "overridden_data")
+        inject_repo(ext, "foo")
+        """);
+    scratch.file(workspaceRoot.getRelative("BUILD").getPathString());
+    scratch.file(
+        workspaceRoot.getRelative("data.bzl").getPathString(),
+        """
+        load("@bar//:list.bzl", _bar_list = "list")
+        load("@foo//:data.bzl", _foo_data = "data")
+        bar_list = _bar_list
+        foo_data = _foo_data
+        """);
+    scratch.file(
+        workspaceRoot.getRelative("defs.bzl").getPathString(),
+        """
+        load("@data_repo//:defs.bzl", "data_repo")
+        def _list_repo_impl(ctx):
+          ctx.file("WORKSPACE")
+          ctx.file("BUILD")
+          labels = [str(Label(l)) for l in ctx.attr.labels]
+          labels += [str(Label("@foo//:target3"))]
+          ctx.file("list.bzl", "list = " + repr(labels) + " + [str(Label('@foo//:target4'))]")
+        list_repo = repository_rule(
+          implementation = _list_repo_impl,
+          attrs = {
+            "labels": attr.label_list(),
+          },
+        )
+        def _ext_impl(ctx):
+          list_repo(
+            name = "bar",
+            labels = [
+              # lazy extension implementation function repository mapping
+              "@foo//:target1",
+              # module repo repository mapping
+              Label("@foo//:target2"),
+            ],
+          )
+        ext = module_extension(implementation = _ext_impl)
+        """);
+
+    SkyKey skyKey = BzlLoadValue.keyForBuild(Label.parseCanonical("//:data.bzl"));
+    EvaluationResult<BzlLoadValue> result =
+        evaluator.evaluate(ImmutableList.of(skyKey), evaluationContext);
+    if (result.hasError()) {
+      throw result.getError().getException();
+    }
+    assertThat((List<?>) result.get(skyKey).getModule().getGlobal("bar_list"))
+        .containsExactly(
+            "@@_main~_repo_rules~foo//:target1",
+            "@@_main~_repo_rules~foo//:target2",
+            "@@_main~_repo_rules~foo//:target3",
+            "@@_main~_repo_rules~foo//:target4")
+        .inOrder();
+    Object fooData = result.get(skyKey).getModule().getGlobal("foo_data");
+    assertThat(fooData).isInstanceOf(String.class);
+    assertThat(fooData).isEqualTo("overridden_data");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
index 60bbeed..22bd752 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
@@ -781,6 +781,7 @@
                         .setExtensionBzlFile("@mymod//:defs.bzl")
                         .setExtensionName("myext1")
                         .setIsolationKey(Optional.empty())
+                        .setRepoOverrides(ImmutableMap.of())
                         .addProxy(
                             ModuleExtensionUsage.Proxy.builder()
                                 .setLocation(
@@ -811,6 +812,7 @@
                         .setExtensionBzlFile("@mymod//:defs.bzl")
                         .setExtensionName("myext2")
                         .setIsolationKey(Optional.empty())
+                        .setRepoOverrides(ImmutableMap.of())
                         .addProxy(
                             ModuleExtensionUsage.Proxy.builder()
                                 .setLocation(
@@ -855,6 +857,7 @@
                         .setExtensionBzlFile("@rules_jvm_external//:defs.bzl")
                         .setExtensionName("maven")
                         .setIsolationKey(Optional.empty())
+                        .setRepoOverrides(ImmutableMap.of())
                         .addProxy(
                             ModuleExtensionUsage.Proxy.builder()
                                 .setLocation(
@@ -932,6 +935,7 @@
                         .setExtensionBzlFile("@//:defs.bzl")
                         .setExtensionName("myext")
                         .setIsolationKey(Optional.empty())
+                        .setRepoOverrides(ImmutableMap.of())
                         .addProxy(
                             ModuleExtensionUsage.Proxy.builder()
                                 .setLocation(
@@ -1063,6 +1067,7 @@
                         .setExtensionBzlFile("@mymod//:defs.bzl")
                         .setExtensionName("myext")
                         .setIsolationKey(Optional.empty())
+                        .setRepoOverrides(ImmutableMap.of())
                         .addProxy(
                             ModuleExtensionUsage.Proxy.builder()
                                 .setLocation(
@@ -1182,6 +1187,7 @@
                         .setExtensionBzlFile("//:MODULE.bazel")
                         .setExtensionName("_repo_rules")
                         .setIsolationKey(Optional.empty())
+                        .setRepoOverrides(ImmutableMap.of())
                         .addProxy(
                             ModuleExtensionUsage.Proxy.builder()
                                 .setLocation(
@@ -1698,7 +1704,8 @@
         .isEqualTo(ImmutableList.of(">=7.0.0"));
     assertThat(result.get(moduleFileKey).getModule().getDeps())
         .containsExactly(
-            "ccc", InterimModule.DepSpec.fromModuleKey(ModuleKey.create("ccc", Version.parse("3.0"))));
+            "ccc",
+            InterimModule.DepSpec.fromModuleKey(ModuleKey.create("ccc", Version.parse("3.0"))));
 
     FileSystemUtils.writeContentAsLatin1(
         otherPatch,
@@ -1721,7 +1728,8 @@
         .isEqualTo(ImmutableList.of(">=7.0.0"));
     assertThat(result.get(moduleFileKey).getModule().getDeps())
         .containsExactly(
-            "ccc", InterimModule.DepSpec.fromModuleKey(ModuleKey.create("ccc", Version.parse("2.0"))));
+            "ccc",
+            InterimModule.DepSpec.fromModuleKey(ModuleKey.create("ccc", Version.parse("2.0"))));
   }
 
   @Test
@@ -1761,4 +1769,203 @@
                 + " Renaming /module/MODULE.bazel while applying patches to it as a single file is"
                 + " not supported.");
   }
+
+  @Test
+  public void testOverrideRepo_overridingRepoDoesntExist() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        ext = use_extension('//:defs.bzl', 'ext')
+        override_repo(ext, 'foo')
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:3:14: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 3, column 14, in <toplevel>
+        \t\toverride_repo(ext, 'foo')
+        Error in override_repo: The repo exported as 'foo' by module extension 'ext' is \
+        overridden with 'foo', but no repo is visible under this name""");
+  }
+
+  @Test
+  public void testOverrideRepo_duplicateOverride() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        bazel_dep(name = "override1", version = "1.0")
+        bazel_dep(name = "override2", version = "1.0")
+        ext = use_extension('//:defs.bzl', 'ext')
+        override_repo(ext, foo = "override1")
+        override_repo(ext, foo = "override2")
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:6:14: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 6, column 14, in <toplevel>
+        \t\toverride_repo(ext, foo = "override2")
+        Error in override_repo: The repo exported as 'foo' by module extension 'ext' is already \
+        overridden with 'override1' at /workspace/MODULE.bazel:5:14""");
+  }
+
+  @Test
+  public void testOverrideRepo_chain_singleExtension() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        bazel_dep(name = "override", version = "1.0")
+        ext = use_extension('//:defs.bzl', 'ext')
+        use_repo(ext, bar = "foo")
+        override_repo(ext, baz = "bar")
+        override_repo(ext, foo = "override")
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:5:14: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 5, column 14, in <toplevel>
+        \t\toverride_repo(ext, baz = "bar")
+        Error in override_repo: The repo 'bar' used as an override for 'baz' in module extension \
+        'ext' is itself overridden with 'override' at /workspace/MODULE.bazel:6:14, which is not \
+        supported.""");
+  }
+
+  @Test
+  public void testOverrideRepo_chain_multipleExtensions() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        bazel_dep(name = "override", version = "1.0")
+        ext1 = use_extension('//:defs.bzl', 'ext1')
+        ext2 = use_extension('//:defs.bzl', 'ext2')
+        override_repo(ext1, baz = "bar")
+        override_repo(ext2, foo = "override")
+        use_repo(ext2, bar = "foo")
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:5:14: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 5, column 14, in <toplevel>
+        \t\toverride_repo(ext1, baz = "bar")
+        Error in override_repo: The repo 'bar' used as an override for 'baz' in module extension \
+        'ext1' is itself overridden with 'override' at /workspace/MODULE.bazel:6:14, which is not \
+        supported.""");
+  }
+
+  @Test
+  public void testOverrideRepo_cycle_singleExtension() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        bazel_dep(name = "override", version = "1.0")
+        ext = use_extension('//:defs.bzl', 'ext')
+        use_repo(ext, my_foo = "foo", my_bar = "bar")
+        override_repo(ext, foo = "my_bar", bar = "my_foo")
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:5:14: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 5, column 14, in <toplevel>
+        \t\toverride_repo(ext, foo = "my_bar", bar = "my_foo")
+        Error in override_repo: The repo 'my_foo' used as an override for 'bar' in module \
+        extension 'ext' is itself overridden with 'my_bar' at /workspace/MODULE.bazel:5:14, which \
+        is not supported.""");
+  }
+
+  @Test
+  public void testOverrideRepo_cycle_multipleExtensions() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        ext1 = use_extension('//:defs.bzl', 'ext1')
+        ext2 = use_extension('//:defs.bzl', 'ext2')
+        override_repo(ext1, foo = "my_bar")
+        override_repo(ext2, bar = "my_foo")
+        use_repo(ext1, my_foo = "foo")
+        use_repo(ext2, my_bar = "bar")
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:5:14: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 5, column 14, in <toplevel>
+        \t\toverride_repo(ext2, bar = "my_foo")
+        Error in override_repo: The repo 'my_foo' used as an override for 'bar' in module \
+        extension 'ext2' is itself overridden with 'my_bar' at /workspace/MODULE.bazel:4:14, \
+        which is not supported.""");
+  }
+
+  @Test
+  public void testInjectRepo_imported() throws Exception {
+    scratch.overwriteFile(
+        rootDirectory.getRelative("MODULE.bazel").getPathString(),
+        """
+        module(name='aaa')
+        bazel_dep(name = 'my_repo', version = "1.0")
+        ext = use_extension('//:defs.bzl', 'ext')
+        inject_repo(ext, foo = 'my_repo')
+        use_repo(ext, bar = 'foo')
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<RootModuleFileValue> result =
+        evaluator.evaluate(
+            ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+    assertThat(result.hasError()).isTrue();
+
+    assertContainsEvent(
+        """
+        ERROR /workspace/MODULE.bazel:5:9: Traceback (most recent call last):
+        \tFile "/workspace/MODULE.bazel", line 5, column 9, in <toplevel>
+        \t\tuse_repo(ext, bar = 'foo')
+        Error in use_repo: Cannot import repo 'foo' that has been injected into \
+        module extension 'ext' at /workspace/MODULE.bazel:4:12. Please refer \
+        to @my_repo directly.\
+        """);
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModuleTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModuleTest.java
index 121bab0..b300c58 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModuleTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModuleTest.java
@@ -45,7 +45,8 @@
     return ModuleExtensionUsage.builder()
         .setExtensionBzlFile("//:rje.bzl")
         .setExtensionName("maven")
-        .setIsolationKey(Optional.empty());
+        .setIsolationKey(Optional.empty())
+        .setRepoOverrides(ImmutableMap.of());
   }
 
   /** A builder for ModuleExtension that sets all the mandatory but irrelevant fields. */
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
index 6ac2a9d..9deb60b 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
@@ -607,6 +607,7 @@
                 ModuleExtensionUsage.builder()
                     .setExtensionBzlFile("//extensions:extensions.bzl")
                     .setExtensionName("maven")
+                    .setRepoOverrides(ImmutableMap.of())
                     .addProxy(
                         ModuleExtensionUsage.Proxy.builder()
                             .setLocation(Location.fromFileLineColumn("C@1.0/MODULE.bazel", 2, 23))
@@ -621,6 +622,7 @@
                 ModuleExtensionUsage.builder()
                     .setExtensionBzlFile("//extensions:extensions.bzl")
                     .setExtensionName("maven")
+                    .setRepoOverrides(ImmutableMap.of())
                     .addProxy(
                         ModuleExtensionUsage.Proxy.builder()
                             .setLocation(Location.fromFileLineColumn("D@1.0/MODULE.bazel", 1, 10))
@@ -635,6 +637,7 @@
                 ModuleExtensionUsage.builder()
                     .setExtensionBzlFile("//extensions:extensions.bzl")
                     .setExtensionName("gradle")
+                    .setRepoOverrides(ImmutableMap.of())
                     .addProxy(
                         ModuleExtensionUsage.Proxy.builder()
                             .setLocation(Location.fromFileLineColumn("Y@2.0/MODULE.bazel", 2, 13))
@@ -649,6 +652,7 @@
                 ModuleExtensionUsage.builder()
                     .setExtensionBzlFile("//extensions:extensions.bzl")
                     .setExtensionName("maven")
+                    .setRepoOverrides(ImmutableMap.of())
                     .addProxy(
                         ModuleExtensionUsage.Proxy.builder()
                             .setLocation(Location.fromFileLineColumn("Y@2.0/MODULE.bazel", 13, 10))
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
index 0a8790d..6282c71 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
@@ -796,6 +796,7 @@
             + "//test:aspect.bzl%MyAspect aspect on java_library rule //test:xxx: \n"
             + "Traceback (most recent call last):\n"
             + "\tFile \"/workspace/test/aspect.bzl\", line 2, column 13, in _impl\n"
+            + "\t\treturn 1 // 0\n"
             + "Error: integer division by zero");
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
index 3c00892..f667de3 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
@@ -444,17 +444,17 @@
                 "Traceback (most recent call last):",
                 "\tFile \"/workspace/test/starlark/extension.bzl\", line 6, column 6, in"
                     + " custom_rule_impl",
-                // "\t\tfoo()",
+                "\t\tfoo()",
                 "\tFile \"/workspace/test/starlark/extension.bzl\", line 9, column 6, in foo",
-                // "\t\tbar(2, 4)",
+                "\t\tbar(2, 4)",
                 "\tFile \"/workspace/test/starlark/extension.bzl\", line 11, column 8, in bar",
-                // "\t\tfirst(x, y, z)",
+                "\t\tfirst(x, y, z)",
                 "\tFile \"/workspace/test/starlark/functions.bzl\", line 2, column 9, in first",
-                // "\t\tsecond(a, b)",
+                "\t\tsecond(a, b)",
                 "\tFile \"/workspace/test/starlark/functions.bzl\", line 5, column 8, in second",
-                // "\t\tthird(\"legal\")",
+                "\t\tthird('legal')",
                 "\tFile \"/workspace/test/starlark/functions.bzl\", line 7, column 12, in third",
-                // ...
+                "\t\t" + expr.stripLeading(),
                 errorMessage);
     scratch.file(
         "test/starlark/extension.bzl",
@@ -466,9 +466,9 @@
         "  foo()",
         "  return [MyInfo(provider_key = ftb)]",
         "def foo():",
-        "  bar(2,4)",
+        "  bar(2, 4)",
         "def bar(x,y,z=1):",
-        "  first(x,y, z)",
+        "  first(x, y, z)",
         "custom_rule = rule(implementation = custom_rule_impl,",
         "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)})");
     scratch.file(
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/BUILD b/src/test/java/com/google/devtools/build/lib/testutil/BUILD
index f5d1477..54c734f 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/testutil/BUILD
@@ -73,6 +73,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
+        "//src/main/java/net/starlark/java/eval",
         "//src/main/java/net/starlark/java/syntax",
         "//src/main/protobuf:failure_details_java_proto",
         "//third_party:error_prone_annotations",
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java b/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java
index 3c70cb7..aa845ff 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java
@@ -13,8 +13,11 @@
 // limitations under the License.
 package com.google.devtools.build.lib.testutil;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.fail;
 
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
 import com.google.common.eventbus.EventBus;
 import com.google.devtools.build.lib.clock.BlazeClock;
 import com.google.devtools.build.lib.events.Event;
@@ -24,11 +27,13 @@
 import com.google.devtools.build.lib.events.Reporter;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
 import java.util.Set;
 import java.util.regex.Pattern;
+import net.starlark.java.eval.EvalException;
 import org.junit.After;
 import org.junit.Before;
 
@@ -78,6 +83,19 @@
     scratch.file(rootDirectory.getRelative("WORKSPACE").getPathString());
     scratch.file(rootDirectory.getRelative("MODULE.bazel").getPathString());
     root = Root.fromPath(rootDirectory);
+
+    // Let the Starlark interpreter know how to read source files.
+    EvalException.setSourceReaderSupplier(
+        () ->
+            loc -> {
+              try {
+                String content = FileSystemUtils.readContent(fileSystem.getPath(loc.file()), UTF_8);
+                return Iterables.get(Splitter.on("\n").split(content), loc.line() - 1, null);
+              } catch (Exception ignored) {
+                // ignore any exceptions reading the file -- this is just for extra info
+                return null;
+              }
+            });
   }
 
   @Before
diff --git a/src/test/tools/bzlmod/MODULE.bazel.lock b/src/test/tools/bzlmod/MODULE.bazel.lock
index 0f1004e..07e9c2b 100644
--- a/src/test/tools/bzlmod/MODULE.bazel.lock
+++ b/src/test/tools/bzlmod/MODULE.bazel.lock
@@ -64,7 +64,7 @@
     "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
       "general": {
         "bzlTransitiveDigest": "PjIds3feoYE8SGbbIq2SFTZy3zmxeO2tQevJZNDo7iY=",
-        "usagesDigest": "7eb+38HaSYXDxn7OZOP6ji/VKxWYeAheiZoTwtTVCgY=",
+        "usagesDigest": "+hz7IHWN6A1oVJJWNDB6yZRG+RYhF76wAYItpAeIUIg=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -92,7 +92,7 @@
     "@@platforms//host:extension.bzl%host_platform": {
       "general": {
         "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=",
-        "usagesDigest": "iyc9ppxZx1NLcdWY1BLyOIAfce+oMGjjBiFgC6LKn+k=",
+        "usagesDigest": "pCYpDQmqMbmiiPI1p2Kd3VLm5T48rRAht5WdW0X2GlA=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -109,7 +109,7 @@
     "@@rules_jvm_external~//:extensions.bzl%maven": {
       "general": {
         "bzlTransitiveDigest": "VW3qd5jCZXYbR9xpSwrhGQ04GCmEIIFPVERY34HHvFE=",
-        "usagesDigest": "Ibxz+lEeSRsbUXF0hvMKg1hWMFUL0S0/LBkSsDDs6lg=",
+        "usagesDigest": "LrHQqpB5iw7+xvJG0erQ0h4vkSrdvObnMfY7Zbx7qhY=",
         "recordedFileInputs": {
           "@@rules_jvm_external~//rules_jvm_external_deps_install.json": "10442a5ae27d9ff4c2003e5ab71643bf0d8b48dcf968b4173fa274c3232a8c06"
         },
@@ -1133,7 +1133,7 @@
     "@@rules_jvm_external~//:non-module-deps.bzl%non_module_deps": {
       "general": {
         "bzlTransitiveDigest": "ZOivBbbZUakRexeLO/N26oX4Bcph6HHnqNmfxt7yoCc=",
-        "usagesDigest": "hwl83iGGV39M7yBqqlvKbiCEgo0wLkrXn8FcWAMOt1A=",
+        "usagesDigest": "Ccxo9D2Jf1yAMLB2+zS+9MGgnKIFhxCAxFkSqwdK/3c=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -1161,7 +1161,7 @@
     "@@rules_python~//python/extensions:python.bzl%python": {
       "general": {
         "bzlTransitiveDigest": "lbXqTyC4ahBb81TIrIp+2d3sWnlurVNqSeAaLJknLUs=",
-        "usagesDigest": "sdqp9yj+PY7j/QqeodNDzIPDsgW02WGMxb76v6YZfBM=",
+        "usagesDigest": "1Y6kbygksx7wAtDStFoHnR90xr8Yeq00I91YcLMbxMI=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -1191,7 +1191,7 @@
     "@@rules_python~//python/extensions/private:internal_deps.bzl%internal_deps": {
       "general": {
         "bzlTransitiveDigest": "b6FMQSdoZ1QOssw14AW8bWDn2BvywI4FVkLbO2nTMsE=",
-        "usagesDigest": "ckBX0TASrGH+IURVs99U68Ub17sjGKe25FfdEE51DWg=",
+        "usagesDigest": "KPNj8wxzOk7dXY9StqZ91MCKEIJSEnAyV0Q/dGFP5sw=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},