ModqueryCommand added

- `ModqueryCommand` added for the external dependency inspection `modquery` bazel command with option parsing logic implemented.

- `ModqueryOptions` added.

- `ModqueryExecutor` empty skeleton added to separate query execution logic and print to the injected output stream. Dummy test implementations provided for `tree` and `deps` query types.

https://github.com/bazelbuild/bazel/issues/15365

PiperOrigin-RevId: 454914018
Change-Id: Ic9cdd71748eafe63fbf67bb74c0a170a12f8647b
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/BUILD
index 278b192..04c4235 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BUILD
@@ -27,6 +27,7 @@
         "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories",
         "//src/main/java/com/google/devtools/build/lib/analysis:config/build_configuration",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common",
+        "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:registry",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution_impl",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index e87754a..8bf9411 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
+import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorFunction;
 import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleResolutionFunction;
 import com.google.devtools.build.lib.bazel.bzlmod.LocalPathOverride;
 import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionResolutionFunction;
@@ -36,6 +37,7 @@
 import com.google.devtools.build.lib.bazel.bzlmod.SingleExtensionEvalFunction;
 import com.google.devtools.build.lib.bazel.bzlmod.SingleExtensionUsagesFunction;
 import com.google.devtools.build.lib.bazel.commands.FetchCommand;
+import com.google.devtools.build.lib.bazel.commands.ModqueryCommand;
 import com.google.devtools.build.lib.bazel.commands.SyncCommand;
 import com.google.devtools.build.lib.bazel.repository.LocalConfigPlatformFunction;
 import com.google.devtools.build.lib.bazel.repository.LocalConfigPlatformRule;
@@ -200,6 +202,7 @@
   @Override
   public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builder) {
     builder.addCommands(new FetchCommand());
+    builder.addCommands(new ModqueryCommand());
     builder.addCommands(new SyncCommand());
     builder.addInfoItems(new RepositoryCacheInfoItem(repositoryCache));
   }
@@ -256,6 +259,7 @@
             SkyFunctions.MODULE_FILE,
             new ModuleFileFunction(registryFactory, directories.getWorkspace(), builtinModules))
         .addSkyFunction(SkyFunctions.BAZEL_MODULE_RESOLUTION, new BazelModuleResolutionFunction())
+        .addSkyFunction(SkyFunctions.BAZEL_MODULE_INSPECTION, new BazelModuleInspectorFunction())
         .addSkyFunction(SkyFunctions.SINGLE_EXTENSION_EVAL, singleExtensionEvalFunction)
         .addSkyFunction(SkyFunctions.SINGLE_EXTENSION_USAGES, new SingleExtensionUsagesFunction())
         .addSkyFunction(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
index 897a1e1..a80c320 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
@@ -225,12 +225,14 @@
     srcs = [
         "BazelModuleInspectorFunction.java",
         "BazelModuleInspectorValue.java",
+        "ModqueryExecutor.java",
     ],
     deps = [
         ":common",
         ":resolution",
         "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec:serialization-constant",
+        "//src/main/java/com/google/devtools/build/lib/util/io",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//third_party:auto_value",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModqueryExecutor.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModqueryExecutor.java
new file mode 100644
index 0000000..278b748
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModqueryExecutor.java
@@ -0,0 +1,106 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.bazel.bzlmod;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule;
+import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule.ResolutionReason;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Executes inspection queries for {@link
+ * com.google.devtools.build.lib.bazel.commands.ModqueryCommand} and prints the resulted output.
+ */
+public class ModqueryExecutor {
+  private final ImmutableMap<ModuleKey, Module> resolvedDepGraph;
+  private final ImmutableMap<ModuleKey, AugmentedModule> depGraph;
+  private final ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex;
+  private final AnsiTerminalPrinter printer;
+
+  public ModqueryExecutor(
+      ImmutableMap<ModuleKey, Module> resolvedDepGraph,
+      ImmutableMap<ModuleKey, AugmentedModule> depGraph,
+      ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex,
+      AnsiTerminalPrinter printer) {
+    this.resolvedDepGraph = resolvedDepGraph;
+    this.depGraph = depGraph;
+    this.modulesIndex = modulesIndex;
+    this.printer = printer;
+  }
+
+  public void tree(ImmutableSet<ModuleKey> from) {
+    printer.printLn(Mode.WARNING + "DUMMY IMPLEMENTATION" + Mode.DEFAULT);
+    printer.printLn("");
+    printer.printLn("All modules index:");
+    printer.printLn(modulesIndex.toString());
+    printer.printLn("");
+    for (ModuleKey target : from) {
+      printer.printLn(Mode.INFO.toString() + target + Mode.DEFAULT);
+      Set<ModuleKey> seen = new HashSet<>();
+      Deque<ModuleKey> toVisit = new ArrayDeque<>();
+      toVisit.add(target);
+      seen.add(target);
+      while (!toVisit.isEmpty()) {
+        ModuleKey curr = toVisit.remove();
+        AugmentedModule module = depGraph.get(curr);
+        for (Entry<ModuleKey, ResolutionReason> e : module.getDeps().entrySet()) {
+          ModuleKey child = e.getKey();
+          if (!resolvedDepGraph.containsKey(child) || seen.contains(child)) {
+            continue;
+          }
+          seen.add(child);
+          toVisit.add(child);
+          printer.printLn(child + " " + e.getValue());
+        }
+      }
+      printer.printLn("");
+    }
+  }
+
+  public void deps(ImmutableSet<ModuleKey> targets) {
+    printer.printLn(Mode.WARNING + "DUMMY IMPLEMENTATION" + Mode.DEFAULT);
+    printer.printLn("");
+    for (ModuleKey target : targets) {
+      printer.printLn(Mode.INFO.toString() + target + Mode.DEFAULT);
+      for (Entry<ModuleKey, ResolutionReason> e : depGraph.get(target).getDeps().entrySet()) {
+        printer.printLn(e.getKey() + " " + e.getValue());
+      }
+      printer.printLn("");
+    }
+  }
+
+  public void path(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  public void allPaths(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  public void explain(ImmutableSet<ModuleKey> targets) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  public void show(ImmutableSet<ModuleKey> targets) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
index e04e817..e2c22f2 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
@@ -15,6 +15,7 @@
     srcs = glob(["*.java"]),
     resources = [
         "fetch.txt",
+        "modquery.txt",
         "sync.txt",
     ],
     deps = [
@@ -25,6 +26,9 @@
         "//src/main/java/com/google/devtools/build/lib/analysis:no_build_event",
         "//src/main/java/com/google/devtools/build/lib/analysis:no_build_request_finished_event",
         "//src/main/java/com/google/devtools/build/lib/bazel:resolved_event",
+        "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common",
+        "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection",
+        "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/starlark",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
@@ -44,12 +48,15 @@
         "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
         "//src/main/java/com/google/devtools/build/lib/util:exit_code",
         "//src/main/java/com/google/devtools/build/lib/util:interrupted_failure_details",
+        "//src/main/java/com/google/devtools/build/lib/util/io",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//src/main/java/com/google/devtools/common/options",
         "//src/main/java/net/starlark/java/eval",
         "//src/main/protobuf:failure_details_java_proto",
+        "//third_party:auto_value",
         "//third_party:guava",
+        "//third_party:jsr305",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommand.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommand.java
new file mode 100644
index 0000000..c0d9438
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommand.java
@@ -0,0 +1,298 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.bazel.commands;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue;
+import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleResolutionValue;
+import com.google.devtools.build.lib.bazel.bzlmod.ModqueryExecutor;
+import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey;
+import com.google.devtools.build.lib.bazel.bzlmod.Version;
+import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.QueryType;
+import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.QueryTypeConverter;
+import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.TargetModule;
+import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.TargetModuleListConverter;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.pkgcache.PackageOptions;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandResult;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.runtime.KeepGoingOption;
+import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption;
+import com.google.devtools.build.lib.runtime.UiOptions;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.ModqueryCommand.Code;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.DetailedExitCode;
+import com.google.devtools.build.lib.util.InterruptedFailureDetails;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter;
+import com.google.devtools.build.skyframe.EvaluationContext;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsParsingResult;
+import java.util.List;
+import java.util.Objects;
+
+/** Queries the Bzlmod external dependency graph. */
+@Command(
+    name = ModqueryCommand.NAME,
+    options = {
+      ModqueryOptions.class,
+      PackageOptions.class,
+      KeepGoingOption.class,
+      LoadingPhaseThreadsOption.class
+    },
+    // TODO(andreisolo): figure out which extra options are really needed
+    help = "resource:modquery.txt",
+    shortDescription = "Queries the Bzlmod external dependency graph",
+    allowResidue = true)
+public final class ModqueryCommand implements BlazeCommand {
+
+  public static final String NAME = "modquery";
+
+  @Override
+  public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
+    BazelModuleResolutionValue moduleResolution;
+    BazelModuleInspectorValue moduleInspector;
+
+    try {
+      // Don't know exactly what it does, used in 'fetch'
+      env.syncPackageLoading(options);
+
+      SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor();
+      LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class);
+
+      EvaluationContext evaluationContext =
+          EvaluationContext.newBuilder()
+              .setNumThreads(threadsOption.threads)
+              .setEventHandler(env.getReporter())
+              .build();
+
+      EvaluationResult<SkyValue> evaluationResult =
+          skyframeExecutor.prepareAndGet(
+              ImmutableSet.of(BazelModuleResolutionValue.KEY, BazelModuleInspectorValue.KEY),
+              evaluationContext);
+
+      if (evaluationResult.hasError()) {
+        Exception e = evaluationResult.getError().getException();
+        String message = "Unexpected error during repository rule evaluation.";
+        if (e != null) {
+          message = e.getMessage();
+        }
+        return reportAndCreateFailureResult(env, message, Code.INVALID_ARGUMENTS);
+      }
+
+      moduleResolution =
+          (BazelModuleResolutionValue) evaluationResult.get(BazelModuleResolutionValue.KEY);
+
+      moduleInspector =
+          (BazelModuleInspectorValue) evaluationResult.get(BazelModuleInspectorValue.KEY);
+
+    } catch (InterruptedException e) {
+      String errorMessage = "Modquery interrupted: " + e.getMessage();
+      env.getReporter().handle(Event.error(errorMessage));
+      return BlazeCommandResult.detailedExitCode(
+          InterruptedFailureDetails.detailedExitCode(errorMessage));
+    } catch (AbruptExitException e) {
+      env.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage()));
+      return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode());
+    }
+
+    AnsiTerminalPrinter printer =
+        new AnsiTerminalPrinter(
+            env.getReporter().getOutErr().getOutputStream(),
+            options.getOptions(UiOptions.class).useColor());
+
+    ModqueryExecutor modqueryExecutor =
+        new ModqueryExecutor(
+            moduleResolution.getDepGraph(),
+            moduleInspector.getDepGraph(),
+            moduleInspector.getModulesIndex(),
+            printer);
+
+    ModqueryOptions modqueryOptions = options.getOptions(ModqueryOptions.class);
+    Preconditions.checkArgument(modqueryOptions != null);
+
+    if (options.getResidue().isEmpty()) {
+      String errorMessage =
+          String.format("No query type specified, choose one from : %s.", QueryType.printValues());
+      return reportAndCreateFailureResult(env, errorMessage, Code.MODQUERY_COMMAND_UNKNOWN);
+    }
+
+    String queryInput = options.getResidue().get(0);
+    QueryType query;
+    try {
+      query = new QueryTypeConverter().convert(queryInput);
+    } catch (OptionsParsingException e) {
+      String errorMessage =
+          String.format("Invalid query type, choose one from : %s.", QueryType.printValues());
+      return reportAndCreateFailureResult(env, errorMessage, Code.MODQUERY_COMMAND_UNKNOWN);
+    }
+
+    List<String> args = options.getResidue().subList(1, options.getResidue().size());
+
+    ImmutableList<ImmutableSet<ModuleKey>> argsKeysList;
+    try {
+      argsKeysList = parseTargetArgs(args, query.getArgNumber(), moduleInspector.getModulesIndex());
+    } catch (InvalidArgumentException e) {
+      return reportAndCreateFailureResult(env, e.getMessage(), e.getCode());
+    } catch (OptionsParsingException e) {
+      return reportAndCreateFailureResult(env, e.getMessage(), Code.INVALID_ARGUMENTS);
+    }
+    /* Extract and check the --from argument */
+    ImmutableSet<ModuleKey> fromKeys;
+    try {
+      fromKeys =
+          targetListToModuleKeySet(modqueryOptions.modulesFrom, moduleInspector.getModulesIndex());
+    } catch (InvalidArgumentException e) {
+      return reportAndCreateFailureResult(env, e.getMessage(), e.getCode());
+    }
+
+    switch (query) {
+      case TREE:
+        modqueryExecutor.tree(fromKeys);
+        break;
+      case DEPS:
+        modqueryExecutor.deps(argsKeysList.get(0));
+        break;
+      case PATH:
+        modqueryExecutor.path(fromKeys, argsKeysList.get(0));
+        break;
+      case ALL_PATHS:
+        modqueryExecutor.allPaths(fromKeys, argsKeysList.get(0));
+        break;
+      case EXPLAIN:
+        modqueryExecutor.explain(argsKeysList.get(0));
+        break;
+      case SHOW:
+        modqueryExecutor.show(argsKeysList.get(0));
+        break;
+    }
+
+    return BlazeCommandResult.success();
+  }
+
+  @VisibleForTesting
+  public static ImmutableList<ImmutableSet<ModuleKey>> parseTargetArgs(
+      List<String> args,
+      int requiredArgNum,
+      ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex)
+      throws OptionsParsingException, InvalidArgumentException {
+    if (requiredArgNum != args.size()) {
+      throw new InvalidArgumentException(
+          String.format(
+              "Invalid number of arguments (provided %d, required %d).",
+              args.size(), requiredArgNum),
+          requiredArgNum > args.size() ? Code.MISSING_ARGUMENTS : Code.TOO_MANY_ARGUMENTS);
+    }
+
+    TargetModuleListConverter converter = new TargetModuleListConverter();
+    ImmutableList.Builder<ImmutableSet<ModuleKey>> argsKeysListBuilder =
+        new ImmutableList.Builder<>();
+
+    for (String arg : args) {
+      ImmutableList<TargetModule> targetList = converter.convert(arg);
+      ImmutableSet<ModuleKey> argModuleKeys = targetListToModuleKeySet(targetList, modulesIndex);
+      argsKeysListBuilder.add(argModuleKeys);
+    }
+    return argsKeysListBuilder.build();
+  }
+
+  private static ImmutableSet<ModuleKey> targetListToModuleKeySet(
+      ImmutableList<TargetModule> targetList,
+      ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex)
+      throws InvalidArgumentException {
+    ImmutableSet.Builder<ModuleKey> allTargetKeys = new ImmutableSet.Builder<>();
+    for (TargetModule targetModule : targetList) {
+      allTargetKeys.addAll(targetToModuleKeySet(targetModule, modulesIndex));
+    }
+    return allTargetKeys.build();
+  }
+
+  // Helper to check the module-version argument exists and retrieve its present version(s)
+  // (ModuleKey(s)) if not specified
+  private static ImmutableSet<ModuleKey> targetToModuleKeySet(
+      TargetModule target, ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex)
+      throws InvalidArgumentException {
+    if (target.getName().isEmpty() && Objects.equals(target.getVersion(), Version.EMPTY)) {
+      return ImmutableSet.of(ModuleKey.ROOT);
+    }
+    ImmutableSet<ModuleKey> existingKeys = modulesIndex.get(target.getName());
+
+    if (existingKeys == null) {
+      throw new InvalidArgumentException(
+          String.format("Module %s does not exist in the dependency graph.", target.getName()),
+          Code.INVALID_ARGUMENTS);
+    }
+
+    if (target.getVersion() == null) {
+      return existingKeys;
+    }
+    ModuleKey key = ModuleKey.create(target.getName(), target.getVersion());
+    if (!existingKeys.contains(key)) {
+      throw new InvalidArgumentException(
+          String.format(
+              "Module version %s@%s does not exist, available versions: %s.",
+              target.getName(), key, existingKeys),
+          Code.INVALID_ARGUMENTS);
+    }
+    return ImmutableSet.of(key);
+  }
+
+  private static BlazeCommandResult reportAndCreateFailureResult(
+      CommandEnvironment env, String message, Code detailedCode) {
+    if (message.charAt(message.length() - 1) != '.') {
+      message = message.concat(".");
+    }
+    String fullMessage =
+        String.format(
+            message.concat(" Type '%s help modquery' for syntax and help."),
+            env.getRuntime().getProductName());
+    env.getReporter().handle(Event.error(fullMessage));
+    return createFailureResult(fullMessage, detailedCode);
+  }
+
+  private static BlazeCommandResult createFailureResult(String message, Code detailedCode) {
+    return BlazeCommandResult.detailedExitCode(
+        DetailedExitCode.of(
+            FailureDetail.newBuilder()
+                .setModqueryCommand(
+                    FailureDetails.ModqueryCommand.newBuilder().setCode(detailedCode).build())
+                .setMessage(message)
+                .build()));
+  }
+
+  /** Exception thrown when a user-input argument is invalid */
+  @VisibleForTesting
+  public static class InvalidArgumentException extends Exception {
+    private final Code code;
+
+    private InvalidArgumentException(String message, Code code) {
+      super(message);
+      this.code = code;
+    }
+
+    public Code getCode() {
+      return code;
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryOptions.java
new file mode 100644
index 0000000..7019a9c6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryOptions.java
@@ -0,0 +1,283 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.bazel.commands;
+
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Ascii;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.bazel.bzlmod.Version;
+import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Options for ModqueryCommand */
+public class ModqueryOptions extends OptionsBase {
+
+  @Option(
+      name = "from",
+      defaultValue = "root",
+      converter = TargetModuleListConverter.class,
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      help =
+          "The module(s) starting from which the dependency graph query will be displayed. Check"
+              + " each query’s description for the exact semantic. Defaults to root.\n")
+  public ImmutableList<TargetModule> modulesFrom;
+
+  @Option(
+      name = "extra",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      help =
+          "The queries will also display the reason why modules were resolved to their current"
+              + " version (if changed). Defaults to true only for the explain query.")
+  public boolean extra;
+
+  @Option(
+      name = "unused",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      help =
+          "The queries will also take into account and display the unused modules, which are not"
+              + " present in the module resolution graph after selection (due to the"
+              + " Minimal-Version Selection or override rules). This can have different effects for"
+              + " each of the query types i.e. include new paths in the all_paths command, or extra"
+              + " dependants in the explain command.\n")
+  public boolean unused;
+
+  @Option(
+      name = "depth",
+      defaultValue = "2147483647",
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      help =
+          "Maximum display depth of the dependency tree. A depth of 1 displays the direct"
+              + " dependencies, for example. For tree, path and all_paths it defaults to"
+              + " Integer.MAX_VALUE, while for explain it defaults to 1 (only displays direct deps"
+              + " of the root besides the leaves and their parents).\n")
+  public int depth;
+
+  @Option(
+      name = "cycles",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      help =
+          "Points out dependency cycles inside the displayed tree, which are normally ignored by"
+              + " default.")
+  public boolean cycles;
+
+  @Option(
+      name = "charset",
+      defaultValue = "utf8",
+      converter = CharsetConverter.class,
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
+      help =
+          "Chooses the character set to use for the tree. Only affects text output. Valid values"
+              + " are \"utf8\" or \"ascii\". Default is \"utf8\"")
+  public Charset charset;
+
+  @Option(
+      name = "prefix",
+      defaultValue = "indent",
+      converter = PrefixConverter.class,
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
+      help =
+          "Sets how each line is displayed (only affects `text` output). The prefix value can be"
+              + " one of \"indent\", \"depth\" or \"none\". The default is \"indent\"")
+  public Prefix prefix;
+
+  @Option(
+      name = "output",
+      defaultValue = "text",
+      converter = OutputFormatConverter.class,
+      documentationCategory = OptionDocumentationCategory.MODQUERY,
+      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
+      help =
+          "The format in which the query results should be printed. Allowed values for query are: "
+              + "text, json, graph")
+  public OutputFormat outputFormat;
+
+  enum QueryType {
+    DEPS(1),
+    TREE(0),
+    ALL_PATHS(1),
+    PATH(1),
+    EXPLAIN(1),
+    SHOW(1);
+
+    /* Store the number of arguments that it accepts for easy pre-check */
+    private final int argNumber;
+
+    QueryType(int argNumber) {
+      this.argNumber = argNumber;
+    }
+
+    @Override
+    public String toString() {
+      return Ascii.toLowerCase(this.name());
+    }
+
+    public int getArgNumber() {
+      return argNumber;
+    }
+
+    public static String printValues() {
+      return "(" + stream(values()).map(QueryType::toString).collect(joining(", ")) + ")";
+    }
+  }
+
+  /** Converts a query type option string to a properly typed {@link QueryType} */
+  public static class QueryTypeConverter extends EnumConverter<QueryType> {
+    public QueryTypeConverter() {
+      super(QueryType.class, "query type");
+    }
+  }
+
+  enum Charset {
+    UTF8,
+    ASCII
+  }
+
+  /** Converts a charset option string to a properly typed {@link Charset} */
+  public static class CharsetConverter extends EnumConverter<Charset> {
+    public CharsetConverter() {
+      super(Charset.class, "output charset");
+    }
+  }
+
+  enum Prefix {
+    INDENT,
+    DEPTH,
+    NONE
+  }
+
+  /** Converts a prefix option string to a properly typed {@link Charset} */
+  public static class PrefixConverter extends EnumConverter<Prefix> {
+    public PrefixConverter() {
+      super(Prefix.class, "output tree prefix");
+    }
+  }
+
+  enum OutputFormat {
+    TEXT,
+    JSON,
+    GRAPH
+  }
+
+  /** Converts an output format option string to a properly typed {@link OutputFormat} */
+  public static class OutputFormatConverter extends EnumConverter<OutputFormat> {
+    public OutputFormatConverter() {
+      super(OutputFormat.class, "output format");
+    }
+  }
+
+  /** Argument of a modquery converted from the form name@version or name. */
+  @AutoValue
+  abstract static class TargetModule {
+    static TargetModule create(String name, Version version) {
+      return new AutoValue_ModqueryOptions_TargetModule(name, version);
+    }
+
+    abstract String getName();
+
+    /**
+     * If it is null, it represents any (one or multiple) present versions of the module in the dep
+     * graph, which is different from the empty version
+     */
+    @Nullable
+    abstract Version getVersion();
+  }
+
+  /** Converts a module target argument string to a properly typed {@link TargetModule} */
+  static class TargetModuleConverter implements Converter<TargetModule> {
+
+    @Override
+    public TargetModule convert(String input) throws OptionsParsingException {
+      String errorMessage = String.format("Cannot parse the given module argument: %s.", input);
+      Preconditions.checkArgument(input != null);
+      if (Ascii.equalsIgnoreCase(input, "root")) {
+        return TargetModule.create("", Version.EMPTY);
+      } else {
+        List<String> splits = Splitter.on('@').splitToList(input);
+        if (splits.isEmpty() || splits.get(0).isEmpty()) {
+          throw new OptionsParsingException(errorMessage);
+        }
+
+        if (splits.size() == 2) {
+          if (splits.get(1).equals("_")) {
+            return TargetModule.create(splits.get(0), Version.EMPTY);
+          }
+          if (splits.get(1).isEmpty()) {
+            throw new OptionsParsingException(errorMessage);
+          }
+          try {
+            return TargetModule.create(splits.get(0), Version.parse(splits.get(1)));
+          } catch (ParseException e) {
+            throw new OptionsParsingException(errorMessage, e);
+          }
+
+        } else if (splits.size() == 1) {
+          return TargetModule.create(splits.get(0), null);
+        } else {
+          throw new OptionsParsingException(errorMessage);
+        }
+      }
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "root, <module>@<version> or <module>";
+    }
+  }
+
+  /** Converts a comma-separated module list argument (i.e. A@1.0,B@2.0) */
+  public static class TargetModuleListConverter implements Converter<ImmutableList<TargetModule>> {
+
+    @Override
+    public ImmutableList<TargetModule> convert(String input) throws OptionsParsingException {
+      CommaSeparatedNonEmptyOptionListConverter listConverter =
+          new CommaSeparatedNonEmptyOptionListConverter();
+      TargetModuleConverter targetModuleConverter = new TargetModuleConverter();
+      ImmutableList<String> targetStrings = listConverter.convert(input);
+      ImmutableList.Builder<TargetModule> targetModules = new ImmutableList.Builder<>();
+      for (String targetInput : targetStrings) {
+        targetModules.add(targetModuleConverter.convert(targetInput));
+      }
+      return targetModules.build();
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "a list of <module>s separated by comma";
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/modquery.txt b/src/main/java/com/google/devtools/build/lib/bazel/commands/modquery.txt
new file mode 100644
index 0000000..84c1dc3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/modquery.txt
@@ -0,0 +1,31 @@
+
+Usage: %{product} %{command} [<option> ...] <query_type> [<args> ...]
+
+The command will display a dependency tree or parts of the dependency tree, structured to display different kinds of insights depending on the query type.
+Calling the command with no argument will default to:
+bazel modquery tree root
+
+<query_type> [<args> ...] can be one of the following:
+
+    - tree: Displays the full dependency tree. Use the --from option to specify which module(s) you want the tree to start from (defaults to root which displays the whole dependency tree).
+    - deps <module(s)>: Displays the direct dependencies of the target module(s).
+    - path <module(s)_to>: Displays the shortest path found in the dependency graph from (any of) the --from module(s) to (any of) <module(s)_to>.
+    - all_paths <module(s)_to>: Display the dependency graph starting from (any of) the --from module(s) and containing any existing paths to (any of) the <module(s)_to>.
+    - explain <module(s)>: Prints all the places where the module is (or was) requested as a direct dependency, along with the reason why the respective final version was selected. It will display a pruned version of the all_paths root <module(s)> command which only contains the direct deps of the root, the <module(s)> leaves, along with their dependants (can be modified with --depth).
+    - show <module(s)>: Prints the rule that generated these modules’ repos (i.e. http_archive()).
+
+
+<module> arguments must be of type:
+
+    - root: The current (root) module you are inside of.
+    - <name>@<version>: A specific module version.
+    - <name>@_: Specifies the empty version of a module (for non-registry overridden) modules).
+    - <name>: Can be used as a placeholder for all the present versions of the module <name>
+
+<modules> means:
+    - <module>,<module>,... : A list of comma separated modules, where each <module> has the form of one of the above.
+
+NOTE: This command is still very experimental and the precise semantics
+will change in the near future.
+
+%{options}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java b/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java
index 34a2b02..83da7c3 100644
--- a/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java
+++ b/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java
@@ -55,8 +55,8 @@
   LOGGING,
 
   /**
-   * This option affects how strictly Bazel enforces valid build inputs (rule definitions,
-   * flag combinations, etc).
+   * This option affects how strictly Bazel enforces valid build inputs (rule definitions, flag
+   * combinations, etc).
    */
   INPUT_STRICTNESS,
 
@@ -79,8 +79,8 @@
   OUTPUT_PARAMETERS,
 
   /**
-   * This option provides information about signing outputs of the build. (For example, signing
-   * an iOS application with a certificate.)
+   * This option provides information about signing outputs of the build. (For example, signing an
+   * iOS application with a certificate.)
    */
   SIGNING,
 
@@ -90,9 +90,7 @@
    */
   STARLARK_SEMANTICS,
 
-  /**
-   * This option dictates information about the test environment or test runner.
-   */
+  /** This option dictates information about the test environment or test runner. */
   TESTING,
 
   /**
@@ -106,6 +104,9 @@
   /** This option relates to query output and semantics. */
   QUERY,
 
+  /** This option relates to modquery (external dependencies) output and semantics. */
+  MODQUERY,
+
   /**
    * This option specifies or alters a generic input to a Bazel command. This category should only
    * be used if the input is generic and does not fall into other categories, such as toolchain-
diff --git a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
index d85a8c4..1e66ed6 100644
--- a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
+++ b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
@@ -32,6 +32,7 @@
     OptionDocumentationCategory.STARLARK_SEMANTICS,
     OptionDocumentationCategory.TESTING,
     OptionDocumentationCategory.QUERY,
+    OptionDocumentationCategory.MODQUERY,
     OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION,
     OptionDocumentationCategory.LOGGING,
     OptionDocumentationCategory.GENERIC_INPUTS,
@@ -86,6 +87,9 @@
             "Options that configure the toolchain used for action execution")
         .put(OptionDocumentationCategory.QUERY, "Options relating to query output and semantics")
         .put(
+            OptionDocumentationCategory.MODQUERY,
+            "Options relating to modquery output and semantics")
+        .put(
             OptionDocumentationCategory.GENERIC_INPUTS,
             "Options specifying or altering a generic input to a Bazel command that does not fall "
                 + "into other categories.")
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index c8a8721..54091ef 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -147,6 +147,7 @@
     StarlarkLoading starlark_loading = 179;
     ExternalDeps external_deps = 181;
     DiffAwareness diff_awareness = 182;
+    ModqueryCommand modquery_command = 183;
   }
 
   reserved 102; // For internal use
@@ -1274,3 +1275,14 @@
 
   Code code = 1;
 }
+
+message ModqueryCommand {
+  enum Code {
+    MODQUERY_COMMAND_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    MISSING_ARGUMENTS = 1 [(metadata) = { exit_code: 2 }];
+    TOO_MANY_ARGUMENTS = 2 [(metadata) = { exit_code: 2 }];
+    INVALID_ARGUMENTS = 3 [(metadata) = { exit_code: 2 }];
+  }
+
+  Code code = 1;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/BUILD
index 8b826e0..2579b51 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bazel/BUILD
@@ -8,6 +8,7 @@
     testonly = 0,
     srcs = glob(["*"]) + [
         "//src/test/java/com/google/devtools/build/lib/bazel/bzlmod:srcs",
+        "//src/test/java/com/google/devtools/build/lib/bazel/commands:srcs",
         "//src/test/java/com/google/devtools/build/lib/bazel/debug:srcs",
         "//src/test/java/com/google/devtools/build/lib/bazel/execlog:srcs",
         "//src/test/java/com/google/devtools/build/lib/bazel/repository:srcs",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/commands/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/commands/BUILD
new file mode 100644
index 0000000..847dd62
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/commands/BUILD
@@ -0,0 +1,40 @@
+load("@rules_java//java:defs.bzl", "java_test")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(["*"]),
+    visibility = ["//src:__subpackages__"],
+)
+
+java_library(
+    name = "BzlmodCommandsTests_lib",
+    srcs = glob(
+        ["*.java"],
+    ),
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common",
+        "//src/main/java/com/google/devtools/build/lib/bazel/commands",
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:failure_details_java_proto",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
+    name = "BzlmodCommandsTests",
+    test_class = "com.google.devtools.build.lib.AllTests",
+    runtime_deps = [
+        ":BzlmodCommandsTests_lib",
+        "//src/test/java/com/google/devtools/build/lib:test_runner",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommandTest.java b/src/test/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommandTest.java
new file mode 100644
index 0000000..c3d0bee
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommandTest.java
@@ -0,0 +1,122 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.bazel.commands;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey;
+import com.google.devtools.build.lib.bazel.bzlmod.Version;
+import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
+import com.google.devtools.build.lib.bazel.commands.ModqueryCommand.InvalidArgumentException;
+import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.QueryType;
+import com.google.devtools.build.lib.server.FailureDetails.ModqueryCommand.Code;
+import com.google.devtools.common.options.OptionsParsingException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ModqueryCommand}. */
+@RunWith(JUnit4.class)
+public class ModqueryCommandTest {
+
+  public final ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex =
+      ImmutableMap.of(
+          "A",
+          ImmutableSet.of(ModuleKey.ROOT),
+          "B",
+          ImmutableSet.of(
+              ModuleKey.create("B", Version.parse("1.0")),
+              ModuleKey.create("B", Version.parse("2.0"))),
+          "C",
+          ImmutableSet.of(ModuleKey.create("C", Version.EMPTY)));
+
+  public ModqueryCommandTest() throws ParseException {}
+
+  @Test
+  public void testAllPathsNoArgsThrowsMissingArguments() {
+    InvalidArgumentException e =
+        assertThrows(
+            InvalidArgumentException.class,
+            () ->
+                ModqueryCommand.parseTargetArgs(
+                    ImmutableList.of(), QueryType.ALL_PATHS.getArgNumber(), modulesIndex));
+    assertThat(e.getCode()).isEqualTo(Code.MISSING_ARGUMENTS);
+  }
+
+  @Test
+  public void testTreeNoArgs() throws InvalidArgumentException, OptionsParsingException {
+    ModqueryCommand.parseTargetArgs(
+        ImmutableList.of(), QueryType.TREE.getArgNumber(), modulesIndex);
+  }
+
+  @Test
+  public void testTreeWithArgsThrowsTooManyArguments() {
+    ImmutableList<String> args = ImmutableList.of("A");
+    InvalidArgumentException e =
+        assertThrows(
+            InvalidArgumentException.class,
+            () ->
+                ModqueryCommand.parseTargetArgs(args, QueryType.TREE.getArgNumber(), modulesIndex));
+    assertThat(e.getCode()).isEqualTo(Code.TOO_MANY_ARGUMENTS);
+  }
+
+  @Test
+  public void testDepsArgWrongFormat_noVersion() {
+    ImmutableList<String> args = ImmutableList.of("A@");
+    assertThrows(
+        OptionsParsingException.class,
+        () -> ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex));
+  }
+
+  @Test
+  public void testDepsArgInvalid_missingModule() {
+    ImmutableList<String> args = ImmutableList.of("D");
+    InvalidArgumentException e =
+        assertThrows(
+            InvalidArgumentException.class,
+            () ->
+                ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex));
+    assertThat(e.getCode()).isEqualTo(Code.INVALID_ARGUMENTS);
+  }
+
+  @Test
+  public void testDepsArgInvalid_missingModuleVersion() {
+    ImmutableList<String> args = ImmutableList.of("B@3.0");
+    InvalidArgumentException e =
+        assertThrows(
+            InvalidArgumentException.class,
+            () ->
+                ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex));
+    assertThat(e.getCode()).isEqualTo(Code.INVALID_ARGUMENTS);
+  }
+
+  @Test
+  public void testDepsArgInvalid_invalidListFormat() {
+    ImmutableList<String> args = ImmutableList.of("B@1.0;B@2.0");
+    assertThrows(
+        OptionsParsingException.class,
+        () -> ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex));
+  }
+
+  @Test
+  public void testDepsListArg_ok() throws InvalidArgumentException, OptionsParsingException {
+    ImmutableList<String> args = ImmutableList.of("A,B@1.0,B@2.0,C@_");
+    ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex);
+  }
+}