Repository: bazelbuild/bazel
Issue: Allow module extensions to check whether a tag is a dev dependency
Labels: type: feature request
P2
team-ExternalDeps
area-Bzlmod

### Description of the feature request:


With Bzlmod, a module extension should be able to check whether the extension proxy that was used to add a given module tag had `dev_dependency = True` or not.

### What underlying problem are you trying to solve with this feature?


While this information isn't directly required to resolve dependencies, the distinction in prod and dev dependencies becomes important in a umber of situations:

1. A module extension may want to emit a list of repos the root module should bring into scope with `use_repo`, but dev dependencies have to be brought into scope with a `use_repo` call on a module extension proxy created with `dev_dependency = True`.
2. Module extensions wrapping external package managers may want to use this information when associating license metadata with packages.

### Which operating system are you running Bazel on?


Any

### What is the output of `bazel info release`?


6.0.0

### If `bazel info release` returns `development version` or `(@non-git)`, tell us how you built Bazel.


_No response_

### What's the output of `git remote get-url origin; git rev-parse master; git rev-parse HEAD` ?


_No response_

### Have you found anything relevant by searching the web?

https://github.com/bazelbuild/bazel/issues/17048

### Any other information, logs, or outputs that you want to share?

_No response_

Issue comments:
@meteorcloudy @Wyverald I would be interested in hearing your thoughts on whether this should be possible.
Can you explain a bit why having `dev_dependency` for the module extension proxy call isn't enough? Under which use case, do we have to share dev and non-dev dependencies with the same module extension proxy call? 
Having the `dev_dependency` parameter on `use_extension` is indeed enough as users can just create a new proxy when they want to add a different type of dependency.

This feature request is only about making the information that a certain proxy (in particular, the set of tags created with it) was created with `dev_dependency = True` available to the module extension. 

The very concrete use case I have for this information is the `auto_use_repo` idea mentioned in #17048. If a module extension wants to emit the correct `use_repo` calls that should be made in the root module's `MODULE.bazel`, it has to be able to distinguish tags from dev and non-dev proxies.
I understand the idea of `auto_use_repo`, but have no idea how it should work (neither on how Gazelle's `update-repos` rep o works). I thought since the tags can only be used via the extension proxy created by `use_extension`, we shouldn't need the `dev_dependency = True` on tags?
Let's consider the following example `MODULE.bazel` of a root module:

```
foo_deps = use_extension("@rules_foo//foo:extensions.bzl", "foo_deps")
foo_deps.package(name = "fmt")
foo_deps.package(name = "json")
# Managed by auto_use_repo
use_repo(foo_deps, "fmt", "json")

foo_dev_deps = use_extension("@rules_foo//foo:extensions.bzl", "foo_deps", dev_dependency = True)
foo_dev_deps.package(name = "testing")
# Managed by auto_use_repo
use_repo(foo_dev_deps, "testing")
```

In this situation, the `foo_deps` extension would create a repository with a file that contains a list of all repos added due to tags defined by the root module. `auto_use_repo` could consume the list and ensure that the `use_repo` calls are up to date.

However, with the currently available features, `foo_deps` can't know that `testing` is a dev dependency and should thus be added to the `use_repo(foo_dev_deps, ...)` call whereas `fmt` is a regular dependency and thus has to be added to the `use_repo(foo_deps, ...)` call.

One way to fulfill this feature request would be to add `module_ctx.is_dev_dependency(module_tag)`, which would return `True` if and only if the argument is a module tag created by a module extension proxy in turn created by a call to `use_extension` with `dev_dependency = True`. If that were available, `foo_deps` could create the following `repositories.json` file that `auto_use_repo` could use to add the used repositories to the correct `use_repo` call:

```json
[{"name": "fmt"}, {"name": "json"}, {"name": "testing", "dev_dependency": true}]
```

I expect this to be useful in other situation, such as if `foo_deps` wants to add usage or license metadata to the repositories it creates.
Thanks for the example -- that helped me understand the use case better. I'd like to think a bit more about how the API should look like.
Thanks!! Do I understand it correctly that we probably don't need to add `dev_dependency` to the `tag` API, but implementing `module_ctx.is_dev_dependency` is enough?
> Thanks!! Do I understand it correctly that we probably don't need to add `dev_dependency` to the `tag` API, but implementing `module_ctx.is_dev_dependency` is enough?

Yes, exactly. Tags are already implicitly dev dependencies if they come from a proxy that is marked with `dev_dependency`, so I don't think adding `dev_dependency` to tags would increase expressivity.

Thank you for contributing to the Bazel repository! This issue has been marked as stale since it has not had any activity in the last 30 days. It will be closed in the next 7 days unless any other activity occurs or one of the following labels is added: "not stale", "awaiting-bazeler".
Not stale
@bazel-io fork 6.2.0

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionContext.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionContext.java
index 6dbf5e3e4a6748..0cf2fd58a9a15d 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionContext.java
@@ -24,6 +24,8 @@
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import java.util.Map;
 import javax.annotation.Nullable;
+import net.starlark.java.annot.Param;
+import net.starlark.java.annot.ParamType;
 import net.starlark.java.annot.StarlarkBuiltin;
 import net.starlark.java.annot.StarlarkMethod;
 import net.starlark.java.eval.EvalException;
@@ -99,4 +101,20 @@ protected ImmutableMap<String, String> getRemoteExecProperties() throws EvalExce
   public StarlarkList<StarlarkBazelModule> getModules() {
     return modules;
   }
+
+  @StarlarkMethod(
+      name = "is_dev_dependency",
+      doc =
+          "Returns whether the given tag was specified on the result of a <a "
+              + "href=\"globals.html#use_extension\">use_extension</a> call with "
+              + "<code>devDependency = True</code>.",
+      parameters = {
+        @Param(
+            name = "tag",
+            doc = "A tag obtained from <a href=\"bazel_module.html#tags\">bazel_module.tags</a>.",
+            allowedTypes = {@ParamType(type = TypeCheckedTag.class)})
+      })
+  public boolean isDevDependency(TypeCheckedTag tag) {
+    return tag.isDevDependency();
+  }
 }
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 3ff8e9e65933a8..d48359d212a605 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
@@ -25,6 +25,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.docgen.annot.DocumentMethods;
+import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileGlobals.ModuleExtensionUsageBuilder.ModuleExtensionProxy;
 import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import java.util.ArrayList;
@@ -63,7 +64,7 @@ public class ModuleFileGlobals {
   private final boolean ignoreDevDeps;
   private final Module.Builder module;
   private final Map<String, ModuleKey> deps = new LinkedHashMap<>();
-  private final List<ModuleExtensionProxy> extensionProxies = new ArrayList<>();
+  private final List<ModuleExtensionUsageBuilder> extensionUsageBuilders = new ArrayList<>();
   private final Map<String, ModuleOverride> overrides = new HashMap<>();
   private final Map<String, RepoNameUsage> repoNameUsages = new HashMap<>();
 
@@ -373,38 +374,37 @@ public void registerToolchains(Sequence<?> toolchainLabels) throws EvalException
       },
       useStarlarkThread = true)
   public ModuleExtensionProxy useExtension(
-      String extensionBzlFile, String extensionName, boolean devDependency, StarlarkThread thread)
-      throws EvalException {
-    ModuleExtensionProxy newProxy =
-        new ModuleExtensionProxy(extensionBzlFile, extensionName, thread.getCallerLocation());
+      String extensionBzlFile, String extensionName, boolean devDependency, StarlarkThread thread) {
+    ModuleExtensionUsageBuilder newUsageBuilder =
+        new ModuleExtensionUsageBuilder(
+            extensionBzlFile, extensionName, thread.getCallerLocation());
 
     if (ignoreDevDeps && devDependency) {
       // This is a no-op proxy.
-      return newProxy;
+      return newUsageBuilder.getProxy(devDependency);
     }
 
-    // Find an existing proxy object corresponding to this extension.
-    for (ModuleExtensionProxy proxy : extensionProxies) {
-      if (proxy.extensionBzlFile.equals(extensionBzlFile)
-          && proxy.extensionName.equals(extensionName)) {
-        return proxy;
+    // Find an existing usage builder corresponding to this extension.
+    for (ModuleExtensionUsageBuilder usageBuilder : extensionUsageBuilders) {
+      if (usageBuilder.extensionBzlFile.equals(extensionBzlFile)
+          && usageBuilder.extensionName.equals(extensionName)) {
+        return usageBuilder.getProxy(devDependency);
       }
     }
 
     // If no such proxy exists, we can just use a new one.
-    extensionProxies.add(newProxy);
-    return newProxy;
+    extensionUsageBuilders.add(newUsageBuilder);
+    return newUsageBuilder.getProxy(devDependency);
   }
 
-  @StarlarkBuiltin(name = "module_extension_proxy", documented = false)
-  class ModuleExtensionProxy implements Structure {
+  class ModuleExtensionUsageBuilder {
     private final String extensionBzlFile;
     private final String extensionName;
     private final Location location;
     private final HashBiMap<String, String> imports;
     private final ImmutableList.Builder<Tag> tags;
 
-    ModuleExtensionProxy(String extensionBzlFile, String extensionName, Location location) {
+    ModuleExtensionUsageBuilder(String extensionBzlFile, String extensionName, Location location) {
       this.extensionBzlFile = extensionBzlFile;
       this.extensionName = extensionName;
       this.location = location;
@@ -422,50 +422,69 @@ ModuleExtensionUsage buildUsage() {
           .build();
     }
 
-    void addImport(String localRepoName, String exportedName, Location location)
-        throws EvalException {
-      RepositoryName.validateUserProvidedRepoName(localRepoName);
-      RepositoryName.validateUserProvidedRepoName(exportedName);
-      addRepoNameUsage(localRepoName, "by a use_repo() call", location);
-      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, repoNameUsages.get(collisionRepoName).getWhere());
-      }
-      imports.put(localRepoName, exportedName);
+    /**
+     * Creates a proxy with the specified dev_dependency bit that shares accumulated imports and
+     * tags with all other such proxies, thus preserving their order across dev/non-dev deps.
+     */
+    ModuleExtensionProxy getProxy(boolean devDependency) {
+      return new ModuleExtensionProxy(devDependency);
     }
 
-    @Nullable
-    @Override
-    public Object getValue(String tagName) throws EvalException {
-      return new StarlarkValue() {
-        @StarlarkMethod(
-            name = "call",
-            selfCall = true,
-            documented = false,
-            extraKeywords = @Param(name = "kwargs"),
-            useStarlarkThread = true)
-        public void call(Dict<String, Object> kwargs, StarlarkThread thread) {
-          tags.add(
-              Tag.builder()
-                  .setTagName(tagName)
-                  .setAttributeValues(kwargs)
-                  .setLocation(thread.getCallerLocation())
-                  .build());
+    @StarlarkBuiltin(name = "module_extension_proxy", documented = false)
+    class ModuleExtensionProxy implements Structure {
+
+      private final boolean devDependency;
+
+      private ModuleExtensionProxy(boolean devDependency) {
+        this.devDependency = devDependency;
+      }
+
+      void addImport(String localRepoName, String exportedName, Location location)
+          throws EvalException {
+        RepositoryName.validateUserProvidedRepoName(localRepoName);
+        RepositoryName.validateUserProvidedRepoName(exportedName);
+        addRepoNameUsage(localRepoName, "by a use_repo() call", location);
+        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, repoNameUsages.get(collisionRepoName).getWhere());
         }
-      };
-    }
+        imports.put(localRepoName, exportedName);
+      }
 
-    @Override
-    public ImmutableCollection<String> getFieldNames() {
-      return ImmutableList.of();
-    }
+      @Nullable
+      @Override
+      public Object getValue(String tagName) throws EvalException {
+        return new StarlarkValue() {
+          @StarlarkMethod(
+              name = "call",
+              selfCall = true,
+              documented = false,
+              extraKeywords = @Param(name = "kwargs"),
+              useStarlarkThread = true)
+          public void call(Dict<String, Object> kwargs, StarlarkThread thread) {
+            tags.add(
+                Tag.builder()
+                    .setTagName(tagName)
+                    .setAttributeValues(kwargs)
+                    .setDevDependency(devDependency)
+                    .setLocation(thread.getCallerLocation())
+                    .build());
+          }
+        };
+      }
 
-    @Nullable
-    @Override
-    public String getErrorMessageForUnknownField(String field) {
-      return null;
+      @Override
+      public ImmutableCollection<String> getFieldNames() {
+        return ImmutableList.of();
+      }
+
+      @Nullable
+      @Override
+      public String getErrorMessageForUnknownField(String field) {
+        return null;
+      }
     }
   }
 
@@ -821,8 +840,8 @@ public Module buildModule() {
         .setDeps(ImmutableMap.copyOf(deps))
         .setOriginalDeps(ImmutableMap.copyOf(deps))
         .setExtensionUsages(
-            extensionProxies.stream()
-                .map(ModuleExtensionProxy::buildUsage)
+            extensionUsageBuilders.stream()
+                .map(ModuleExtensionUsageBuilder::buildUsage)
                 .collect(toImmutableList()))
         .build();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Tag.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Tag.java
index 6d89b5a778cc29..498aacd427c49a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Tag.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Tag.java
@@ -33,6 +33,9 @@ public abstract class Tag {
   /** All keyword arguments supplied to the tag instance. */
   public abstract Dict<String, Object> getAttributeValues();
 
+  /** Whether this tag was created using a proxy created with dev_dependency = True. */
+  public abstract boolean isDevDependency();
+
   /** The source location in the module file where this tag was created. */
   public abstract Location getLocation();
 
@@ -48,6 +51,8 @@ public abstract static class Builder {
 
     public abstract Builder setAttributeValues(Dict<String, Object> value);
 
+    public abstract Builder setDevDependency(boolean value);
+
     public abstract Builder setLocation(Location value);
 
     public abstract Tag build();
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java
index 30c52dc3b97a77..54261a2349f863 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java
@@ -34,10 +34,12 @@
 public class TypeCheckedTag implements Structure {
   private final TagClass tagClass;
   private final Object[] attrValues;
+  private final boolean devDependency;
 
-  private TypeCheckedTag(TagClass tagClass, Object[] attrValues) {
+  private TypeCheckedTag(TagClass tagClass, Object[] attrValues, boolean devDependency) {
     this.tagClass = tagClass;
     this.attrValues = attrValues;
+    this.devDependency = devDependency;
   }
 
   /** Creates a {@link TypeCheckedTag}. */
@@ -95,7 +97,15 @@ public static TypeCheckedTag create(TagClass tagClass, Tag tag, LabelConverter l
         attrValues[i] = Attribute.valueToStarlark(attr.getDefaultValueUnchecked());
       }
     }
-    return new TypeCheckedTag(tagClass, attrValues);
+    return new TypeCheckedTag(tagClass, attrValues, tag.isDevDependency());
+  }
+
+  /**
+   * Whether the tag was specified on an extension proxy created with <code>dev_dependency=True
+   * </code>.
+   */
+  public boolean isDevDependency() {
+    return devDependency;
   }
 
   @Override


Test patch:
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodTestUtil.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodTestUtil.java
index cc4dbaba79f53b..2af63ead6d9d35 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodTestUtil.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodTestUtil.java
@@ -283,6 +283,7 @@ public static TagClass createTagClass(Attribute... attrs) {
   public static class TestTagBuilder {
     private final Dict.Builder<String, Object> attrValuesBuilder = Dict.builder();
     private final String tagName;
+    private boolean devDependency = false;
 
     private TestTagBuilder(String tagName) {
       this.tagName = tagName;
@@ -294,11 +295,18 @@ public TestTagBuilder addAttr(String attrName, Object attrValue) {
       return this;
     }
 
+    @CanIgnoreReturnValue
+    public TestTagBuilder setDevDependency() {
+      devDependency = true;
+      return this;
+    }
+
     public Tag build() {
       return Tag.builder()
           .setTagName(tagName)
           .setLocation(Location.BUILTIN)
           .setAttributeValues(attrValuesBuilder.buildImmutable())
+          .setDevDependency(devDependency)
           .build();
     }
   }
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 f8ad86388377d7..69b3adba649dc4 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
@@ -446,7 +446,7 @@ public void multipleModules_devDependency() throws Exception {
         "  data_str = 'modules:'",
         "  for mod in ctx.modules:",
         "    for tag in mod.tags.tag:",
-        "      data_str += ' ' + tag.data",
+        "      data_str += ' ' + tag.data + ' ' + str(ctx.is_dev_dependency(tag))",
         "  data_repo(name='ext_repo',data=data_str)",
         "tag=tag_class(attrs={'data':attr.string()})",
         "ext=module_extension(implementation=_ext_impl,tag_classes={'tag':tag})");
@@ -457,7 +457,8 @@ public void multipleModules_devDependency() throws Exception {
     if (result.hasError()) {
       throw result.getError().getException();
     }
-    assertThat(result.get(skyKey).getModule().getGlobal("data")).isEqualTo("modules: root bar@2.0");
+    assertThat(result.get(skyKey).getModule().getGlobal("data"))
+        .isEqualTo("modules: root True bar@2.0 False");
   }
 
   @Test
@@ -497,7 +498,7 @@ public void multipleModules_ignoreDevDependency() throws Exception {
         "  data_str = 'modules:'",
         "  for mod in ctx.modules:",
         "    for tag in mod.tags.tag:",
-        "      data_str += ' ' + tag.data",
+        "      data_str += ' ' + tag.data + ' ' + str(ctx.is_dev_dependency(tag))",
         "  data_repo(name='ext_repo',data=data_str)",
         "tag=tag_class(attrs={'data':attr.string()})",
         "ext=module_extension(implementation=_ext_impl,tag_classes={'tag':tag})");
@@ -511,7 +512,8 @@ public void multipleModules_ignoreDevDependency() throws Exception {
     if (result.hasError()) {
       throw result.getError().getException();
     }
-    assertThat(result.get(skyKey).getModule().getGlobal("data")).isEqualTo("modules: bar@2.0");
+    assertThat(result.get(skyKey).getModule().getGlobal("data"))
+        .isEqualTo("modules: bar@2.0 False");
   }
 
   @Test
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 7da2e62ffe4098..bbdeb0d4d4d33a 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
@@ -484,6 +484,7 @@ public void testModuleExtensions_good() throws Exception {
                                     Dict.<String, Object>builder()
                                         .put("key", "val")
                                         .buildImmutable())
+                                .setDevDependency(false)
                                 .setLocation(
                                     Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 4, 11))
                                 .build())
@@ -501,6 +502,7 @@ public void testModuleExtensions_good() throws Exception {
                                     Dict.<String, Object>builder()
                                         .put("key1", "val1")
                                         .buildImmutable())
+                                .setDevDependency(false)
                                 .setLocation(
                                     Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 7, 12))
                                 .build())
@@ -511,6 +513,7 @@ public void testModuleExtensions_good() throws Exception {
                                     Dict.<String, Object>builder()
                                         .put("key2", "val2")
                                         .buildImmutable())
+                                .setDevDependency(false)
                                 .setLocation(
                                     Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 8, 12))
                                 .build())
@@ -529,6 +532,7 @@ public void testModuleExtensions_good() throws Exception {
                                     Dict.<String, Object>builder()
                                         .put("coord", "junit")
                                         .buildImmutable())
+                                .setDevDependency(false)
                                 .setLocation(
                                     Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 12, 10))
                                 .build())
@@ -539,6 +543,7 @@ public void testModuleExtensions_good() throws Exception {
                                     Dict.<String, Object>builder()
                                         .put("coord", "guava")
                                         .buildImmutable())
+                                .setDevDependency(false)
                                 .setLocation(
                                     Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 14, 10))
                                 .build())
@@ -551,12 +556,16 @@ public void testModuleExtensions_duplicateProxy_asRoot() throws Exception {
     scratch.file(
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "myext1 = use_extension('//:defs.bzl','myext',dev_dependency=True)",
+        "myext1.tag(name = 'tag1')",
         "use_repo(myext1, 'alpha')",
         "myext2 = use_extension('//:defs.bzl','myext')",
+        "myext2.tag(name = 'tag2')",
         "use_repo(myext2, 'beta')",
         "myext3 = use_extension('//:defs.bzl','myext',dev_dependency=True)",
+        "myext3.tag(name = 'tag3')",
         "use_repo(myext3, 'gamma')",
         "myext4 = use_extension('//:defs.bzl','myext')",
+        "myext4.tag(name = 'tag4')",
         "use_repo(myext4, 'delta')");
     ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
 
@@ -580,6 +589,50 @@ public void testModuleExtensions_duplicateProxy_asRoot() throws Exception {
                             ImmutableBiMap.of(
                                 "alpha", "alpha", "beta", "beta", "gamma", "gamma", "delta",
                                 "delta"))
+                        .addTag(
+                            Tag.builder()
+                                .setTagName("tag")
+                                .setAttributeValues(
+                                    Dict.<String, Object>builder()
+                                        .put("name", "tag1")
+                                        .buildImmutable())
+                                .setDevDependency(true)
+                                .setLocation(
+                                    Location.fromFileLineColumn("<root>/MODULE.bazel", 2, 11))
+                                .build())
+                        .addTag(
+                            Tag.builder()
+                                .setTagName("tag")
+                                .setAttributeValues(
+                                    Dict.<String, Object>builder()
+                                        .put("name", "tag2")
+                                        .buildImmutable())
+                                .setDevDependency(false)
+                                .setLocation(
+                                    Location.fromFileLineColumn("<root>/MODULE.bazel", 5, 11))
+                                .build())
+                        .addTag(
+                            Tag.builder()
+                                .setTagName("tag")
+                                .setAttributeValues(
+                                    Dict.<String, Object>builder()
+                                        .put("name", "tag3")
+                                        .buildImmutable())
+                                .setDevDependency(true)
+                                .setLocation(
+                                    Location.fromFileLineColumn("<root>/MODULE.bazel", 8, 11))
+                                .build())
+                        .addTag(
+                            Tag.builder()
+                                .setTagName("tag")
+                                .setAttributeValues(
+                                    Dict.<String, Object>builder()
+                                        .put("name", "tag4")
+                                        .buildImmutable())
+                                .setDevDependency(false)
+                                .setLocation(
+                                    Location.fromFileLineColumn("<root>/MODULE.bazel", 11, 11))
+                                .build())
                         .build())
                 .build());
   }
@@ -593,12 +646,16 @@ public void testModuleExtensions_duplicateProxy_asDep() throws Exception {
                 createModuleKey("mymod", "1.0"),
                 "module(name='mymod',version='1.0')",
                 "myext1 = use_extension('//:defs.bzl','myext',dev_dependency=True)",
+                "myext1.tag(name = 'tag1')",
                 "use_repo(myext1, 'alpha')",
                 "myext2 = use_extension('//:defs.bzl','myext')",
+                "myext2.tag(name = 'tag2')",
                 "use_repo(myext2, 'beta')",
                 "myext3 = use_extension('//:defs.bzl','myext',dev_dependency=True)",
+                "myext3.tag(name = 'tag3')",
                 "use_repo(myext3, 'gamma')",
                 "myext4 = use_extension('//:defs.bzl','myext')",
+                "myext4.tag(name = 'tag4')",
                 "use_repo(myext4, 'delta')");
     ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
 
@@ -617,8 +674,30 @@ public void testModuleExtensions_duplicateProxy_asDep() throws Exception {
                     ModuleExtensionUsage.builder()
                         .setExtensionBzlFile("//:defs.bzl")
                         .setExtensionName("myext")
-                        .setLocation(Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 4, 23))
+                        .setLocation(Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 5, 23))
                         .setImports(ImmutableBiMap.of("beta", "beta", "delta", "delta"))
+                        .addTag(
+                            Tag.builder()
+                                .setTagName("tag")
+                                .setAttributeValues(
+                                    Dict.<String, Object>builder()
+                                        .put("name", "tag2")
+                                        .buildImmutable())
+                                .setDevDependency(false)
+                                .setLocation(
+                                    Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 6, 11))
+                                .build())
+                        .addTag(
+                            Tag.builder()
+                                .setTagName("tag")
+                                .setAttributeValues(
+                                    Dict.<String, Object>builder()
+                                        .put("name", "tag4")
+                                        .buildImmutable())
+                                .setDevDependency(false)
+                                .setLocation(
+                                    Location.fromFileLineColumn("mymod@1.0/MODULE.bazel", 12, 11))
+                                .build())
                         .build())
                 .build());
   }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTagTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTagTest.java
index f6eec3267f87d8..1151480aaee6e3 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTagTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTagTest.java
@@ -61,10 +61,11 @@ public void basic() throws Exception {
     TypeCheckedTag typeCheckedTag =
         TypeCheckedTag.create(
             createTagClass(attr("foo", Type.INTEGER).build()),
-            buildTag("tag_name").addAttr("foo", StarlarkInt.of(3)).build(),
-            /*labelConverter=*/ null);
+            buildTag("tag_name").addAttr("foo", StarlarkInt.of(3)).setDevDependency().build(),
+            /* labelConverter= */ null);
     assertThat(typeCheckedTag.getFieldNames()).containsExactly("foo");
     assertThat(getattr(typeCheckedTag, "foo")).isEqualTo(StarlarkInt.of(3));
+    assertThat(typeCheckedTag.isDevDependency()).isTrue();
   }
 
   @Test
@@ -87,6 +88,7 @@ public void label() throws Exception {
                 Label.parseAbsoluteUnchecked("@myrepo//mypkg:thing1"),
                 Label.parseAbsoluteUnchecked("@myrepo//pkg:thing2"),
                 Label.parseAbsoluteUnchecked("@other_repo//pkg:thing3")));
+    assertThat(typeCheckedTag.isDevDependency()).isFalse();
   }
 
   @Test
@@ -95,12 +97,13 @@ public void label_withoutDefaultValue() throws Exception {
         TypeCheckedTag.create(
             createTagClass(
                 attr("foo", BuildType.LABEL).allowedFileTypes(FileTypeSet.ANY_FILE).build()),
-            buildTag("tag_name").build(),
+            buildTag("tag_name").setDevDependency().build(),
             new LabelConverter(
                 PackageIdentifier.parse("@myrepo//mypkg"),
                 createRepositoryMapping(createModuleKey("test", "1.0"), "repo", "other_repo")));
     assertThat(typeCheckedTag.getFieldNames()).containsExactly("foo");
     assertThat(getattr(typeCheckedTag, "foo")).isEqualTo(Starlark.NONE);
+    assertThat(typeCheckedTag.isDevDependency()).isTrue();
   }
 
   @Test
@@ -119,6 +122,7 @@ public void stringListDict_default() throws Exception {
             Dict.builder()
                 .put("key", StarlarkList.immutableOf("value1", "value2"))
                 .buildImmutable());
+    assertThat(typeCheckedTag.isDevDependency()).isFalse();
   }
 
   @Test
@@ -139,6 +143,7 @@ public void multipleAttributesAndDefaults() throws Exception {
     assertThat(getattr(typeCheckedTag, "bar")).isEqualTo(StarlarkInt.of(3));
     assertThat(getattr(typeCheckedTag, "quux"))
         .isEqualTo(StarlarkList.immutableOf("quuxValue1", "quuxValue2"));
+    assertThat(typeCheckedTag.isDevDependency()).isFalse();
   }
 
   @Test