Support pass-through `Args` for `JavaBuilder` in `java_common.create_compilation_action` (https://github.com/bazelbuild/bazel/pull/30808)

This allows faster iteration on the `rules_java` <> `JavaBuilder` contract without having to wait for Bazel changes/releases. In the immediate future, this will be used for the unused-deps checking. Longer term this should also help in migrating off the native compilation code.

Note: the added integration shell test is currently a no-op on CI (since we need the corresponding rules_java changes) but does allow for local testing with an overridden `@rules_java`.

Closes #30808.

PiperOrigin-RevId: 971826913
Change-Id: I8047e057716475a7a4d8bff48b8c7bfe6d79ba1c
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
index d924b1a..4e4b44e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Interner;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CommandLine;
 import com.google.devtools.build.lib.actions.ExecutionRequirements;
 import com.google.devtools.build.lib.actions.ParamFileInfo;
 import com.google.devtools.build.lib.actions.ParameterFile;
@@ -73,6 +74,7 @@
   private final ImmutableList<Artifact> additionalInputsForDatabinding;
   private boolean enableJspecify = true;
   private boolean enableDirectClasspath = true;
+  private ImmutableList<CommandLine> extraCommandLineArgs = ImmutableList.of();
   private final String execGroup;
 
   public JavaCompilationHelper(
@@ -104,6 +106,10 @@
     this.enableJspecify = enableJspecify;
   }
 
+  public void setExtraCommandLineArgs(ImmutableList<CommandLine> extraCommandLineArgs) {
+    this.extraCommandLineArgs = Preconditions.checkNotNull(extraCommandLineArgs);
+  }
+
   JavaTargetAttributes getAttributes() {
     if (builtAttributes == null) {
       builtAttributes = attributes.build();
@@ -298,6 +304,7 @@
         .getFixDepsTool(ruleContext.getRule(), getJavaConfiguration())
         .ifPresent(builder::setFixDepsTool);
     builder.setCompileTimeDependencyArtifacts(attributes.getCompileTimeDependencyArtifacts());
+    builder.setExtraCommandLineArgs(extraCommandLineArgs);
     builder.setTargetLabel(
         attributes.getTargetLabel() == null ? label : attributes.getTargetLabel());
     builder.setInjectingRuleKind(attributes.getInjectingRuleKind());
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
index ce638e1..9a867a2 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
@@ -123,6 +123,7 @@
   private final ImmutableMap<String, String> executionInfo;
   private final CommandLine executableLine;
   private final CommandLine flagLine;
+  private final ImmutableList<CommandLine> extraCommandLineArgs;
   private final BuildConfigurationValue configuration;
   private final OnDemandString progressMessage;
 
@@ -149,6 +150,7 @@
       ExtraActionInfoSupplier extraActionInfoSupplier,
       CommandLine executableLine,
       CommandLine flagLine,
+      ImmutableList<CommandLine> extraCommandLineArgs,
       BuildConfigurationValue configuration,
       NestedSet<Artifact> dependencyArtifacts,
       Artifact outputDepsProto,
@@ -171,6 +173,7 @@
         configuration.modifiedExecutionInfo(executionInfo, compilationType.mnemonic);
     this.executableLine = executableLine;
     this.flagLine = flagLine;
+    this.extraCommandLineArgs = extraCommandLineArgs;
     this.configuration = configuration;
     this.progressMessage = progressMessage;
     this.extraActionInfoSupplier = extraActionInfoSupplier;
@@ -230,6 +233,10 @@
         actionKeyContext, inputMetadataProvider, effectiveOutputPathsMode, fp);
     flagLine.addToFingerprint(
         actionKeyContext, inputMetadataProvider, effectiveOutputPathsMode, fp);
+    for (CommandLine extraCommandLine : extraCommandLineArgs) {
+      extraCommandLine.addToFingerprint(
+          actionKeyContext, inputMetadataProvider, effectiveOutputPathsMode, fp);
+    }
     // As the classpath is no longer part of commandLines implicitly, we need to explicitly add
     // the transitive inputs to the key here.
     actionKeyContext.addNestedSetToFingerprint(fp, transitiveInputs);
@@ -309,12 +316,15 @@
       classpathLine.add("--reduce_classpath_mode", fallback ? "BAZEL_FALLBACK" : "BAZEL_REDUCED");
     }
 
-    CommandLines reducedCommandLine =
+    CommandLines.Builder commandLinesBuilder =
         CommandLines.builder()
             .addCommandLine(executableLine)
             .addCommandLine(flagLine, PARAM_FILE_INFO)
-            .addCommandLine(classpathLine.build(), PARAM_FILE_INFO)
-            .build();
+            .addCommandLine(classpathLine.build(), PARAM_FILE_INFO);
+    for (CommandLine extraCommandLine : extraCommandLineArgs) {
+      commandLinesBuilder.addCommandLine(extraCommandLine, PARAM_FILE_INFO);
+    }
+    CommandLines reducedCommandLine = commandLinesBuilder.build();
     CommandLines.ExpandedCommandLines expandedCommandLines =
         reducedCommandLine.expand(
             actionExecutionContext.getInputMetadataProvider(),
@@ -552,11 +562,12 @@
   public ExtraActionInfo.Builder getExtraActionInfo(ActionKeyContext actionKeyContext)
       throws CommandLineExpansionException, InterruptedException {
     ExtraActionInfo.Builder builder = super.getExtraActionInfo(actionKeyContext);
-    CommandLines commandLinesWithoutExecutable =
-        CommandLines.builder()
-            .addCommandLine(flagLine)
-            .addCommandLine(getFullClasspathLine())
-            .build();
+    CommandLines.Builder commandLinesBuilder =
+        CommandLines.builder().addCommandLine(flagLine).addCommandLine(getFullClasspathLine());
+    for (CommandLine extraCommandLine : extraCommandLineArgs) {
+      commandLinesBuilder.addCommandLine(extraCommandLine);
+    }
+    CommandLines commandLinesWithoutExecutable = commandLinesBuilder.build();
     if (extraActionInfoSupplier != null) {
       extraActionInfoSupplier.extend(builder, commandLinesWithoutExecutable.allArguments());
     }
@@ -604,11 +615,15 @@
 
   @VisibleForTesting
   public CommandLines getCommandLines() {
-    return CommandLines.builder()
-        .addCommandLine(executableLine)
-        .addCommandLine(flagLine, PARAM_FILE_INFO)
-        .addCommandLine(getFullClasspathLine(), PARAM_FILE_INFO)
-        .build();
+    CommandLines.Builder builder =
+        CommandLines.builder()
+            .addCommandLine(executableLine)
+            .addCommandLine(flagLine, PARAM_FILE_INFO)
+            .addCommandLine(getFullClasspathLine(), PARAM_FILE_INFO);
+    for (CommandLine extraCommandLine : extraCommandLineArgs) {
+      builder.addCommandLine(extraCommandLine, PARAM_FILE_INFO);
+    }
+    return builder.build();
   }
 
   private CommandLine getFullClasspathLine() {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java
index 5b0d376..fae5300 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
 import com.google.devtools.build.lib.actions.ActionEnvironment;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CommandLine;
 import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
 import com.google.devtools.build.lib.actions.extra.JavaCompileInfo;
 import com.google.devtools.build.lib.analysis.RuleContext;
@@ -157,6 +158,7 @@
   private NestedSet<Artifact> extraData = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER);
   private Label targetLabel;
   @Nullable private String injectingRuleKind;
+  private ImmutableList<CommandLine> extraCommandLineArgs = ImmutableList.of();
   private ImmutableList<Artifact> additionalInputs = ImmutableList.of();
   private Artifact genSourceOutput;
   private JavaCompileOutputs<Artifact> outputs;
@@ -260,6 +262,7 @@
         /* extraActionInfoSupplier= */ extraActionInfoSupplier,
         /* executableLine= */ executableLine,
         /* flagLine= */ buildParamFileContents(javacOpts),
+        /* extraCommandLineArgs= */ extraCommandLineArgs,
         /* configuration= */ ruleContext.getConfiguration(),
         /* dependencyArtifacts= */ compileTimeDependencyArtifacts,
         /* outputDepsProto= */ outputs.depsProto(),
@@ -379,6 +382,13 @@
   }
 
   @CanIgnoreReturnValue
+  public JavaCompileActionBuilder setExtraCommandLineArgs(
+      ImmutableList<CommandLine> extraCommandLineArgs) {
+    this.extraCommandLineArgs = checkNotNull(extraCommandLineArgs);
+    return this;
+  }
+
+  @CanIgnoreReturnValue
   public JavaCompileActionBuilder setJavacOpts(ImmutableList<String> copts) {
     this.javacOpts = Preconditions.checkNotNull(copts);
     return this;
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaHeaderCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaHeaderCompileAction.java
index 52f9d71..1b9dfb4 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaHeaderCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaHeaderCompileAction.java
@@ -655,6 +655,7 @@
               /* extraActionInfoSupplier= */ null,
               /* executableLine= */ executableLine,
               /* flagLine= */ commandLine.build(),
+              /* extraCommandLineArgs= */ ImmutableList.of(),
               /* configuration= */ ruleContext.getConfiguration(),
               /* dependencyArtifacts= */ compileTimeDependencyArtifacts,
               /* outputDepsProto= */ outputDepsProto,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java
index 18b1849..944fec1 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CommandLine;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.Expander;
 import com.google.devtools.build.lib.analysis.RuleContext;
@@ -30,6 +31,7 @@
 import com.google.devtools.build.lib.analysis.configuredtargets.MergedConfiguredTarget;
 import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
 import com.google.devtools.build.lib.analysis.platform.ToolchainInfo;
+import com.google.devtools.build.lib.analysis.starlark.Args;
 import com.google.devtools.build.lib.analysis.starlark.StarlarkActionFactory;
 import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleContext;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -201,7 +203,8 @@
       boolean enableJSpecify,
       boolean enableDirectClasspath,
       Sequence<?> additionalInputs,
-      Sequence<?> additionalOutputs)
+      Sequence<?> additionalOutputs,
+      Sequence<?> extraArgs)
       throws EvalException,
           TypeException,
           RuleErrorException,
@@ -217,6 +220,11 @@
             .nativeHeader(nativeHeader == Starlark.NONE ? null : (Artifact) nativeHeader)
             .manifestProto(manifestProto)
             .build();
+    ImmutableList.Builder<CommandLine> extraCommandLineArgs = ImmutableList.builder();
+    for (Args args : Sequence.cast(extraArgs, Args.class, "extra_args")) {
+      extraCommandLineArgs.add(
+          args.build(ctx.getRuleContext().getAnalysisEnvironment()::getMainRepoMapping));
+    }
     JavaTargetAttributes.Builder attributesBuilder =
         new JavaTargetAttributes.Builder()
             .addSourceJars(Sequence.cast(sourceJars, Artifact.class, "source_jars"))
@@ -261,6 +269,7 @@
         Depset.cast(javaBuilderJvmFlags, String.class, "javabuilder_jvm_flags"));
     compilationHelper.enableJspecify(enableJSpecify);
     compilationHelper.enableDirectClasspath(enableDirectClasspath);
+    compilationHelper.setExtraCommandLineArgs(extraCommandLineArgs.build());
     compilationHelper.createCompileAction(outputs);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java
index 8867bf9..5052f4f 100644
--- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java
+++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.collect.nestedset.Depset.TypeException;
 import com.google.devtools.build.lib.packages.Info;
 import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
+import com.google.devtools.build.lib.starlarkbuildapi.CommandLineArgsApi;
 import com.google.devtools.build.lib.starlarkbuildapi.FileApi;
 import com.google.devtools.build.lib.starlarkbuildapi.StarlarkActionFactoryApi;
 import com.google.devtools.build.lib.starlarkbuildapi.StarlarkRuleContextApi;
@@ -524,6 +525,14 @@
         @Param(name = "enable_direct_classpath", defaultValue = "True", named = true),
         @Param(name = "additional_inputs", defaultValue = "[]", named = true),
         @Param(name = "additional_outputs", defaultValue = "[]", named = true),
+        @Param(
+            name = "extra_args",
+            allowedTypes = {
+              @ParamType(type = Sequence.class, generic1 = CommandLineArgsApi.class),
+            },
+            defaultValue = "[]",
+            named = true,
+            positional = false),
       })
   void createCompilationAction(
       StarlarkRuleContextT ctx,
@@ -553,7 +562,8 @@
       boolean enableJSpecify,
       boolean enableDirectClasspath,
       Sequence<?> additionalInputs,
-      Sequence<?> additionalOutputs)
+      Sequence<?> additionalOutputs,
+      Sequence<?> extraArgs)
       throws EvalException,
           TypeException,
           RuleErrorException,
diff --git a/src/main/starlark/builtins_bzl/common/java/java_common.bzl b/src/main/starlark/builtins_bzl/common/java/java_common.bzl
index 33fefbc..fcc08e9 100644
--- a/src/main/starlark/builtins_bzl/common/java/java_common.bzl
+++ b/src/main/starlark/builtins_bzl/common/java/java_common.bzl
@@ -17,6 +17,7 @@
 _java_common_internal = _builtins.internal.java_common_internal_do_not_use
 
 _ALLOWLIST = [
+    ("", "bazel_internal/test_rules"),
     ("", "third_party/bazel_rules/rules_java"),
     ("", "javatests/com/google/devtools/grok/kythe/analyzers/build/testdata/pkg"),
     ("rules_java", ""),
diff --git a/src/test/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilderTest.java b/src/test/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilderTest.java
index d0b7b6f..6dd7b57 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilderTest.java
@@ -426,4 +426,58 @@
     cacheMiss.setFilename(artifact.getExecPathString());
     return new BulkTransferException(cacheMiss);
   }
+
+  @Test
+  public void testExtraArgsPropagated() throws Exception {
+    scratch.file("bazel_internal/test_rules/BUILD");
+    scratch.file(
+        "bazel_internal/test_rules/rule.bzl",
+        String.format(
+            """
+            load("@rules_java//java:defs.bzl", "JavaPluginInfo", rules_java_common = "java_common")
+            internal_common = java_common.internal_DO_NOT_USE()
+            def _my_rule_impl(ctx):
+                output = ctx.outputs.jar
+                args = ctx.actions.args()
+                args.add("--foo=bar")
+                internal_common.create_compilation_action(
+                    ctx,
+                    ctx.toolchains["%1$s"].java,
+                    output,
+                    ctx.actions.declare_file(ctx.label.name + ".manifest"),
+                    JavaPluginInfo(runtime_deps = []),
+                    depset(),
+                    depset(),
+                    depset(),
+                    depset(),
+                    depset(),
+                    depset(),
+                    "ERROR",
+                    ctx.label,
+                    extra_args = [args],
+                )
+                return [DefaultInfo(files = depset([output]))]
+
+            my_rule = rule(
+                implementation = _my_rule_impl,
+                outputs = {
+                    "jar": "%%{name}.jar",
+                },
+                fragments = ["java"],
+                toolchains = ["%1$s"],
+            )
+            """,
+            TestConstants.JAVA_TOOLCHAIN_TYPE));
+    scratch.file(
+        "java/com/google/test/BUILD",
+        """
+        load("//bazel_internal/test_rules:rule.bzl", "my_rule")
+        my_rule(name = "a")
+        """);
+
+    JavaCompileAction compileAction =
+        (JavaCompileAction) getGeneratingActionForLabel("//java/com/google/test:a.jar");
+    List<String> command = getJavacArguments(compileAction);
+    assertThat(command).contains("--foo=bar");
+  }
 }
diff --git a/src/test/shell/bazel/bazel_java_test.sh b/src/test/shell/bazel/bazel_java_test.sh
index 0f515a5..a02e4f15 100755
--- a/src/test/shell/bazel/bazel_java_test.sh
+++ b/src/test/shell/bazel/bazel_java_test.sh
@@ -2531,4 +2531,325 @@
   expect_log "foo.txt"
 }
 
+# TODO: Once ready to be enabled on CI, make these separate tests with shared setup
+function test_unused_deps() {
+  if [[ -n "${RULES_JAVA_OVERRIDE_PATH:-}" ]]; then
+    cat >> MODULE.bazel <<EOF
+local_path_override(
+    module_name = "rules_java",
+    path = "${RULES_JAVA_OVERRIDE_PATH}",
+)
+EOF
+  fi
+
+  # Check if rules_java has the toolchain-driven unused_deps support
+  # We check this by querying the attributes of java_package_configuration
+  # or attempting to build a package configuration target with 'unused_deps'.
+  # If rules_java does not have it, we skip the test.
+
+  mkdir -p pkg
+  cat > pkg/BUILD <<EOF
+load("@rules_java//java:defs.bzl", "java_package_configuration")
+java_package_configuration(
+    name = "test_config_query",
+    package_specs = [":spec_query"],
+    unused_deps = "error",
+)
+package_group(
+    name = "spec_query",
+    packages = ["//pkg/..."],
+)
+EOF
+
+  if ! bazel query //pkg:test_config_query >/dev/null 2>&1; then
+    echo "Skipping test_unused_deps: rules_java does not support unused_deps configuration yet"
+    return 0
+  fi
+
+  # Write the common toolchain definition for main repo targets
+  cat << 'EOF' > pkg/BUILD
+load("@rules_java//toolchains:default_java_toolchain.bzl", "default_java_toolchain")
+load("@rules_java//java:defs.bzl", "java_package_configuration")
+
+package_group(
+    name = "my_package_spec",
+    packages = ["//pkg/..."],
+)
+
+java_package_configuration(
+    name = "unused_deps_error_config",
+    package_specs = [":my_package_spec"],
+    unused_deps = "error",
+)
+
+default_java_toolchain(
+    name = "java_toolchain",
+    package_configuration = [":unused_deps_error_config"],
+)
+EOF
+
+  # -------------------------------------------------------------
+  # Scenario 1: Simple Unused (Error)
+  # -------------------------------------------------------------
+  cat << 'EOF' >> pkg/BUILD
+load("@rules_java//java:java_library.bzl", "java_library")
+java_library(name = "simple_unused", srcs = ["SimpleUnused.java"], deps = [":b"])
+java_library(name = "b", srcs = ["B.java"])
+EOF
+  echo "public class SimpleUnused {}" > pkg/SimpleUnused.java
+  echo "public class B {}" > pkg/B.java
+
+  bazel build //pkg:simple_unused \
+    --extra_toolchains=//pkg:java_toolchain_definition \
+    >& $TEST_log && fail "build succeeded, but expected it to fail due to unused dependency (Simple Unused)"
+
+  expect_log "Target '//pkg:b' is declared as a direct dependency of '//pkg:simple_unused' but is unused"
+
+  # -------------------------------------------------------------
+  # Scenario 2: Simple Used (Success)
+  # -------------------------------------------------------------
+  echo "public class SimpleUnused { B b; }" > pkg/SimpleUnused.java
+  bazel build //pkg:simple_unused \
+    --extra_toolchains=//pkg:java_toolchain_definition \
+    >& $TEST_log || fail "build failed, but expected Simple Used to succeed"
+
+  # -------------------------------------------------------------
+  # Scenario 3: Exported Only (Error)
+  # -------------------------------------------------------------
+  cat << 'EOF' >> pkg/BUILD
+java_library(name = "exported_only", srcs = ["ExportedOnly.java"], deps = [":exports_c"])
+java_library(name = "exports_c", exports = [":c"])
+java_library(name = "c", srcs = ["C.java"])
+EOF
+  echo "public class ExportedOnly { C c; }" > pkg/ExportedOnly.java
+  echo "public class C {}" > pkg/C.java
+
+  bazel build //pkg:exported_only \
+    --extra_toolchains=//pkg:java_toolchain_definition \
+    >& $TEST_log && fail "build succeeded, but expected it to fail due to unused dependency (Exported Only)"
+
+  expect_log "Target '//pkg:exports_c' is declared as a direct dependency of '//pkg:exported_only' but is unused"
+
+  # -------------------------------------------------------------
+  # Scenario 4: Exported & Direct Used (Success)
+  # -------------------------------------------------------------
+  # Define exports_c_with_srcs that exports C but also has its own class
+  cat << 'EOF' >> pkg/BUILD
+java_library(name = "exported_and_direct", srcs = ["ExportedAndDirect.java"], deps = [":exports_c_with_srcs"])
+java_library(name = "exports_c_with_srcs", srcs = ["B2.java"], exports = [":c"])
+EOF
+  echo "public class B2 {}" > pkg/B2.java
+  echo "public class ExportedAndDirect { B2 b; C c; }" > pkg/ExportedAndDirect.java
+
+  bazel build //pkg:exported_and_direct \
+    --extra_toolchains=//pkg:java_toolchain_definition \
+    >& $TEST_log || fail "build failed, but expected Exported & Direct Used to succeed"
+
+  # -------------------------------------------------------------
+  # Scenario 5: Indirect Used (Error - Strict Deps violation)
+  # -------------------------------------------------------------
+  cat << 'EOF' >> pkg/BUILD
+java_library(name = "indirect_used", srcs = ["IndirectUsed.java"], deps = [":dep_no_exports"])
+java_library(name = "dep_no_exports", deps = [":c"])
+EOF
+  echo "public class IndirectUsed { C c; }" > pkg/IndirectUsed.java
+
+  bazel build //pkg:indirect_used \
+    --extra_toolchains=//pkg:java_toolchain_definition \
+    >& $TEST_log && fail "build succeeded, but expected it to fail due to strict deps violation"
+
+  # Check that it's a strict deps error, not an unused deps error
+  expect_log "is not visible from target '//pkg:indirect_used'"
+
+  # -------------------------------------------------------------
+  # Scenario 6: Non-Main Repository Target (Success)
+  # -------------------------------------------------------------
+  mkdir -p ext
+  cat > ext/MODULE.bazel <<EOF
+module(name = "ext")
+bazel_dep(name = "rules_java")
+EOF
+  if [[ -n "${RULES_JAVA_OVERRIDE_PATH:-}" ]]; then
+    cat >> ext/MODULE.bazel <<EOF
+local_path_override(
+    module_name = "rules_java",
+    path = "${RULES_JAVA_OVERRIDE_PATH}",
+)
+EOF
+  fi
+
+  cat > ext/BUILD <<EOF
+load("@rules_java//java:java_library.bzl", "java_library")
+load("@rules_java//toolchains:default_java_toolchain.bzl", "default_java_toolchain")
+load("@rules_java//java:defs.bzl", "java_package_configuration")
+
+package_group(
+    name = "ext_package_spec",
+    packages = ["//..."],
+)
+
+java_package_configuration(
+    name = "unused_deps_error_config",
+    package_specs = [":ext_package_spec"],
+    unused_deps = "error",
+)
+
+default_java_toolchain(
+    name = "java_toolchain",
+    package_configuration = [":unused_deps_error_config"],
+)
+
+java_library(name = "ext_lib", srcs = ["Ext.java"], deps = [":ext_dep"])
+java_library(name = "ext_dep", srcs = ["ExtDep.java"])
+EOF
+
+  echo "public class Ext {}" > ext/Ext.java
+  echo "public class ExtDep {}" > ext/ExtDep.java
+
+  cat >> MODULE.bazel <<EOF
+bazel_dep(name = "ext")
+local_path_override(
+    module_name = "ext",
+    path = "ext",
+)
+EOF
+
+  # Build the target in the external repo. Even though ext_dep is unused,
+  # it should compile successfully because unused deps checking is disabled for non-main repos.
+  bazel build @ext//:ext_lib \
+    --extra_toolchains=@ext//:java_toolchain_definition \
+    >& $TEST_log || fail "build of external target failed, but expected to succeed (unused check disabled for external repos)"
+}
+
+function test_extra_args_unused_deps_execution() {
+  if [[ "${JAVA_TOOLS_ZIP}" == "released" ]]; then
+    echo "Skipping test_extra_args_unused_deps_execution: released java_tools does not support --experimental_check_unused_deps"
+    return 0
+  fi
+
+  mkdir -p bazel_internal/test_rules pkg
+  touch bazel_internal/test_rules/BUILD
+
+  cat > bazel_internal/test_rules/rule.bzl <<'EOF'
+load("@rules_java//java:defs.bzl", "JavaInfo", "JavaPluginInfo", rules_java_common = "java_common")
+
+def _custom_compile_rule_impl(ctx):
+    internal_common = java_common.internal_DO_NOT_USE()
+    output = ctx.outputs.jar
+    manifest = ctx.actions.declare_file(ctx.label.name + ".manifest")
+
+    compile_jars = []
+    args = ctx.actions.args()
+    args.add("--experimental_check_unused_deps", "ERROR")
+    for dep in ctx.attr.deps:
+        for jar in dep[JavaInfo].compile_jars.to_list():
+            compile_jars.append(jar)
+            args.add("--target_declared_deps", str(dep.label))
+
+    compile_jars_depset = depset(compile_jars)
+
+    internal_common.create_compilation_action(
+        ctx,
+        ctx.attr._java_toolchain[rules_java_common.JavaToolchainInfo],
+        output,
+        manifest,
+        JavaPluginInfo(runtime_deps = []),
+        compile_jars_depset,
+        compile_jars_depset,
+        depset(),
+        depset(),
+        depset(),
+        depset(),
+        "ERROR",
+        ctx.label,
+        sources = depset(ctx.files.srcs),
+        extra_args = [args],
+    )
+    return [
+        DefaultInfo(files = depset([output])),
+        JavaInfo(
+            output_jar = output,
+            compile_jar = output,
+        ),
+    ]
+
+custom_compile_rule = rule(
+    implementation = _custom_compile_rule_impl,
+    outputs = {
+        "jar": "%{name}.jar",
+    },
+    attrs = {
+        "srcs": attr.label_list(allow_files = [".java"]),
+        "deps": attr.label_list(),
+        "_java_toolchain": attr.label(default = "@bazel_tools//tools/jdk:current_java_toolchain"),
+    },
+    fragments = ["java"],
+    toolchains = ["@bazel_tools//tools/jdk:toolchain_type"],
+)
+EOF
+
+  cat > pkg/BUILD <<'EOF'
+load("@rules_java//java:java_library.bzl", "java_library")
+load("//bazel_internal/test_rules:rule.bzl", "custom_compile_rule")
+
+java_library(
+    name = "dep",
+    srcs = ["Dep.java"],
+)
+
+custom_compile_rule(
+    name = "main",
+    srcs = ["Main.java"],
+    deps = [":dep"],
+)
+EOF
+
+  cat > pkg/Dep.java <<'EOF'
+package pkg;
+public class Dep {}
+EOF
+
+  # 1. Main does not use Dep -> MUST FAIL with unused-deps error
+  cat > pkg/Main.java <<'EOF'
+package pkg;
+public class Main {}
+EOF
+
+  bazel build //pkg:main >& $TEST_log && fail "Build succeeded, but expected failure due to unused dependency"
+  expect_log "\[unused-deps\] Dependency '.*:dep' is declared as a direct dependency but is not referenced in jdeps"
+
+  # 2. Main uses Dep -> MUST SUCCEED
+  cat > pkg/Main.java <<'EOF'
+package pkg;
+public class Main {
+  Dep d = new Dep();
+}
+EOF
+
+  bazel build //pkg:main >& $TEST_log || fail "Build failed, but expected success when dependency is used"
+
+  # 3. Main does not use Dep, and Dep is removed from deps -> MUST SUCCEED
+  cat > pkg/Main.java <<'EOF'
+package pkg;
+public class Main {}
+EOF
+  cat > pkg/BUILD <<'EOF'
+load("@rules_java//java:java_library.bzl", "java_library")
+load("//bazel_internal/test_rules:rule.bzl", "custom_compile_rule")
+
+java_library(
+    name = "dep",
+    srcs = ["Dep.java"],
+)
+
+custom_compile_rule(
+    name = "main",
+    srcs = ["Main.java"],
+    deps = [],
+)
+EOF
+
+  bazel build //pkg:main >& $TEST_log || fail "Build failed, but expected success with no unused dependencies"
+}
+
 run_suite "Java integration tests"