Expose current repository name to Java with @AutoBazelRepository

Java targets depending on `@bazel_tools//tools/java/runfiles` can add the new `@AutoBazelRepository` to a class to have an annotation processor generate a companion class with a `BAZEL_REPOSITORY` constant containing the repository name of the target that compiled the class.

This requires a small addition to JavaBuilder to parse the repository name out of the target label and pass it to javac as a processor option.

Work towards #16124

Closes #16534.

PiperOrigin-RevId: 487573496
Change-Id: Id9b6526ce32268089c91c6d17363d1e7682f64a4
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
index b382b1f..313d1b0 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
@@ -371,9 +371,18 @@
 
   /** Computes javacopts for the current rule. */
   private ImmutableList<String> computeJavacOpts(Collection<String> extraRuleJavacOpts) {
-    return ImmutableList.<String>builder()
-        .addAll(javaToolchain.getJavacOptions(ruleContext))
-        .addAll(extraRuleJavacOpts)
+    ImmutableList.Builder<String> javacOpts =
+        ImmutableList.<String>builder()
+            .addAll(javaToolchain.getJavacOptions(ruleContext))
+            .addAll(extraRuleJavacOpts);
+    if (activePlugins
+        .plugins()
+        .processorClasses()
+        .toSet()
+        .contains("com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor")) {
+      javacOpts.add("-Abazel.repository=" + ruleContext.getRepository().getName());
+    }
+    return javacOpts
         .addAll(computePerPackageJavacOpts(ruleContext, javaToolchain))
         .addAll(addModuleJavacopts(ruleContext))
         .addAll(ruleContext.getExpander().withDataLocations().tokenized("javacopts"))
@@ -538,8 +547,8 @@
   public JavaTargetAttributes.Builder initCommon(
       Collection<Artifact> extraSrcs, Iterable<String> extraJavacOpts) {
     Preconditions.checkState(javacOpts == null);
-    javacOpts = computeJavacOpts(ImmutableList.copyOf(extraJavacOpts));
     activePlugins = collectPlugins();
+    javacOpts = computeJavacOpts(ImmutableList.copyOf(extraJavacOpts));
 
     JavaTargetAttributes.Builder javaTargetAttributes = new JavaTargetAttributes.Builder(semantics);
     javaCompilationHelper =
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java
index b698c00..5dca289 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java
@@ -284,6 +284,28 @@
 
     JavaToolchainProvider toolchainProvider = javaToolchain;
 
+    JavaPluginInfo pluginInfo = mergeExportedJavaPluginInfo(plugins, deps);
+    ImmutableList.Builder<String> allJavacOptsBuilder =
+        ImmutableList.<String>builder()
+            .addAll(toolchainProvider.getJavacOptions(starlarkRuleContext.getRuleContext()))
+            .addAll(
+                javaSemantics.getCompatibleJavacOptions(
+                    starlarkRuleContext.getRuleContext(), toolchainProvider));
+    if (pluginInfo
+        .plugins()
+        .processorClasses()
+        .toSet()
+        .contains("com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor")) {
+      allJavacOptsBuilder.add(
+          "-Abazel.repository=" + starlarkRuleContext.getRuleContext().getRepository().getName());
+    }
+    allJavacOptsBuilder
+        .addAll(
+            JavaCommon.computePerPackageJavacOpts(
+                starlarkRuleContext.getRuleContext(), toolchainProvider))
+        .addAll(JavaModuleFlagsProvider.toFlags(addExports, addOpens))
+        .addAll(tokenize(javacOpts));
+
     JavaLibraryHelper helper =
         new JavaLibraryHelper(starlarkRuleContext.getRuleContext())
             .setOutput(outputJar)
@@ -295,18 +317,7 @@
             .setSourcePathEntries(sourcepathEntries)
             .addAdditionalOutputs(annotationProcessorAdditionalOutputs)
             .enableJspecify(enableJSpecify)
-            .setJavacOpts(
-                ImmutableList.<String>builder()
-                    .addAll(toolchainProvider.getJavacOptions(starlarkRuleContext.getRuleContext()))
-                    .addAll(
-                        javaSemantics.getCompatibleJavacOptions(
-                            starlarkRuleContext.getRuleContext(), toolchainProvider))
-                    .addAll(
-                        JavaCommon.computePerPackageJavacOpts(
-                            starlarkRuleContext.getRuleContext(), toolchainProvider))
-                    .addAll(JavaModuleFlagsProvider.toFlags(addExports, addOpens))
-                    .addAll(tokenize(javacOpts))
-                    .build());
+            .setJavacOpts(allJavacOptsBuilder.build());
 
     if (injectingRuleKind != Starlark.NONE) {
       helper.setInjectingRuleKind((String) injectingRuleKind);
@@ -316,7 +327,6 @@
     streamProviders(deps, JavaCompilationArgsProvider.class).forEach(helper::addDep);
     streamProviders(exports, JavaCompilationArgsProvider.class).forEach(helper::addExport);
     helper.setCompilationStrictDepsMode(getStrictDepsMode(Ascii.toUpperCase(strictDepsMode)));
-    JavaPluginInfo pluginInfo = mergeExportedJavaPluginInfo(plugins, deps);
     // Optimization: skip this if there are no annotation processors, to avoid unnecessarily
     // disabling the direct classpath optimization if `enable_annotation_processor = False`
     // but there aren't any annotation processors.
diff --git a/src/test/shell/bazel/bazel_java_test.sh b/src/test/shell/bazel/bazel_java_test.sh
index c11137f..5e55ff3 100755
--- a/src/test/shell/bazel/bazel_java_test.sh
+++ b/src/test/shell/bazel/bazel_java_test.sh
@@ -1763,5 +1763,191 @@
   bazel build //java/main:C2 &>"${TEST_log}" || fail "Expected to build"
 }
 
+function test_auto_bazel_repository() {
+  cat >> WORKSPACE <<'EOF'
+local_repository(
+  name = "other_repo",
+  path = "other_repo",
+)
+EOF
+
+  mkdir -p pkg
+  cat > pkg/BUILD.bazel <<'EOF'
+java_library(
+  name = "library",
+  srcs = ["Library.java"],
+  deps = ["@bazel_tools//tools/java/runfiles"],
+  visibility = ["//visibility:public"],
+)
+
+java_binary(
+  name = "binary",
+  srcs = ["Binary.java"],
+  main_class = "com.example.Binary",
+  deps = [
+    ":library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+
+java_test(
+  name = "test",
+  srcs = ["Test.java"],
+  main_class = "com.example.Test",
+  use_testrunner = False,
+  deps = [
+    ":library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+EOF
+
+  cat > pkg/Library.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+@AutoBazelRepository
+public class Library {
+  public static void printRepositoryName() {
+    System.out.printf("in pkg/Library.java: '%s'%n", AutoBazelRepository_Library.NAME);
+  }
+}
+EOF
+
+  cat > pkg/Binary.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+public class Binary {
+  @AutoBazelRepository
+  private static class Class1 {
+  }
+
+  public static void main(String[] args) {
+    System.out.printf("in pkg/Binary.java: '%s'%n", AutoBazelRepository_Binary_Class1.NAME);
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  cat > pkg/Test.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+public class Test {
+  private static class Class1 {
+    @AutoBazelRepository
+    private static class Class2 {
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.printf("in pkg/Test.java: '%s'%n", AutoBazelRepository_Test_Class1_Class2.NAME);
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  mkdir -p other_repo
+  touch other_repo/WORKSPACE
+
+  mkdir -p other_repo/pkg
+  cat > other_repo/pkg/BUILD.bazel <<'EOF'
+java_library(
+  name = "library2",
+  srcs = ["Library2.java"],
+  deps = ["@bazel_tools//tools/java/runfiles"],
+)
+
+java_binary(
+  name = "binary",
+  srcs = ["Binary.java"],
+  main_class = "com.example.Binary",
+  deps = [
+    ":library2",
+    "@//pkg:library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+java_test(
+  name = "test",
+  srcs = ["Test.java"],
+  main_class = "com.example.Test",
+  use_testrunner = False,
+  deps = [
+    ":library2",
+    "@//pkg:library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+EOF
+
+  cat > other_repo/pkg/Library2.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+@AutoBazelRepository
+public class Library2 {
+  public static void printRepositoryName() {
+    System.out.printf("in external/other_repo/pkg/Library2.java: '%s'%n", AutoBazelRepository_Library2.NAME);
+  }
+}
+EOF
+
+  cat > other_repo/pkg/Binary.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+import static com.example.AutoBazelRepository_Binary.NAME;
+
+@AutoBazelRepository
+public class Binary {
+  public static void main(String[] args) {
+    System.out.printf("in external/other_repo/pkg/Binary.java: '%s'%n", NAME);
+    Library2.printRepositoryName();
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  cat > other_repo/pkg/Test.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+@AutoBazelRepository
+public class Test {
+  public static void main(String[] args) {
+    System.out.printf("in external/other_repo/pkg/Test.java: '%s'%n", AutoBazelRepository_Test.NAME);
+    Library2.printRepositoryName();
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  bazel run //pkg:binary &>"$TEST_log" || fail "Run should succeed"
+  expect_log "in pkg/Binary.java: ''"
+  expect_log "in pkg/Library.java: ''"
+
+  bazel test --test_output=streamed //pkg:test &>"$TEST_log" || fail "Test should succeed"
+  expect_log "in pkg/Test.java: ''"
+  expect_log "in pkg/Library.java: ''"
+
+  bazel run @other_repo//pkg:binary &>"$TEST_log" || fail "Run should succeed"
+  expect_log "in external/other_repo/pkg/Binary.java: 'other_repo'"
+  expect_log "in external/other_repo/pkg/Library2.java: 'other_repo'"
+  expect_log "in pkg/Library.java: ''"
+
+  bazel test --test_output=streamed \
+    @other_repo//pkg:test &>"$TEST_log" || fail "Test should succeed"
+  expect_log "in external/other_repo/pkg/Test.java: 'other_repo'"
+  expect_log "in external/other_repo/pkg/Library2.java: 'other_repo'"
+  expect_log "in pkg/Library.java: ''"
+}
+
 
 run_suite "Java integration tests"
diff --git a/tools/java/runfiles/AutoBazelRepository.java b/tools/java/runfiles/AutoBazelRepository.java
new file mode 100644
index 0000000..6dc5330
--- /dev/null
+++ b/tools/java/runfiles/AutoBazelRepository.java
@@ -0,0 +1,29 @@
+// 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.runfiles;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotating a class {@code Fooer} with this annotation generates a class {@code
+ * AutoBazelRepository_Fooer} defining a {@link String} constant {@code NAME} containing the
+ * canonical name of the repository containing the Bazel target that compiled the annotated class.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+public @interface AutoBazelRepository {}
diff --git a/tools/java/runfiles/AutoBazelRepositoryProcessor.java b/tools/java/runfiles/AutoBazelRepositoryProcessor.java
new file mode 100644
index 0000000..ae356d8
--- /dev/null
+++ b/tools/java/runfiles/AutoBazelRepositoryProcessor.java
@@ -0,0 +1,127 @@
+// 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.runfiles;
+
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedOptions;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.tools.Diagnostic.Kind;
+
+/** Processor for {@link AutoBazelRepository}. */
+@SupportedAnnotationTypes("com.google.devtools.build.runfiles.AutoBazelRepository")
+@SupportedOptions(AutoBazelRepositoryProcessor.BAZEL_REPOSITORY_OPTION)
+public final class AutoBazelRepositoryProcessor extends AbstractProcessor {
+
+  static final String BAZEL_REPOSITORY_OPTION = "bazel.repository";
+
+  @Override
+  public SourceVersion getSupportedSourceVersion() {
+    return SourceVersion.latestSupported();
+  }
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    annotations.stream()
+        .flatMap(element -> roundEnv.getElementsAnnotatedWith(element).stream())
+        .map(element -> (TypeElement) element)
+        .forEach(this::emitClass);
+    return true;
+  }
+
+  private void emitClass(TypeElement annotatedClass) {
+    // This option is always provided by the Java rule implementations.
+    if (!processingEnv.getOptions().containsKey(BAZEL_REPOSITORY_OPTION)) {
+      processingEnv
+          .getMessager()
+          .printMessage(
+              Kind.ERROR,
+              String.format(
+                  "The %1$s annotation processor option is not set. To use this annotation"
+                      + " processor, provide the canonical repository name of the current target as"
+                      + " the value of the -A%1$s flag.",
+                  BAZEL_REPOSITORY_OPTION),
+              annotatedClass);
+      return;
+    }
+    String repositoryName = processingEnv.getOptions().get(BAZEL_REPOSITORY_OPTION);
+    if (repositoryName == null) {
+      // javac translates '-Abazel.repository=' into a null value.
+      // https://github.com/openjdk/jdk/blob/7a49c9baa1d4ad7df90e7ca626ec48ba76881822/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java#L651
+      repositoryName = "";
+    }
+
+    // For a nested class Outer.Middle.Inner, generate a class with simple name
+    // AutoBazelRepository_Outer_Middle_Inner.
+    // Note: There can be collisions when local classes are involved, but since the definition of a
+    // class depends only on the containing Bazel target, this does not result in ambiguity.
+    List<String> nestedClassNames =
+        Stream.iterate(
+                annotatedClass,
+                element -> element instanceof TypeElement,
+                Element::getEnclosingElement)
+            .map(Element::getSimpleName)
+            .map(Name::toString)
+            .collect(toList());
+    Collections.reverse(nestedClassNames);
+    String generatedClassSimpleName =
+        Stream.concat(Stream.of("AutoBazelRepository"), nestedClassNames.stream())
+            .collect(joining("_"));
+
+    String generatedClassPackage =
+        processingEnv.getElementUtils().getPackageOf(annotatedClass).getQualifiedName().toString();
+
+    String generatedClassName =
+        generatedClassPackage.isEmpty()
+            ? generatedClassSimpleName
+            : generatedClassPackage + "." + generatedClassSimpleName;
+
+    try (PrintWriter out =
+        new PrintWriter(
+            processingEnv.getFiler().createSourceFile(generatedClassName).openWriter())) {
+      out.printf("package %s;\n", generatedClassPackage);
+      out.printf("\n");
+      out.printf("class %s {\n", generatedClassSimpleName);
+      out.printf("  /**\n");
+      out.printf("   * The canonical name of the repository containing the Bazel target that\n");
+      out.printf("   * compiled {@link %s}.\n", annotatedClass.getQualifiedName().toString());
+      out.printf("   */\n");
+      out.printf("  static final String NAME = \"%s\";\n", repositoryName);
+      out.printf("\n");
+      out.printf("  private %s() {}\n", generatedClassSimpleName);
+      out.printf("}\n");
+    } catch (IOException e) {
+      processingEnv
+          .getMessager()
+          .printMessage(
+              Kind.ERROR,
+              String.format("Failed to generate %s: %s", generatedClassName, e.getMessage()),
+              annotatedClass);
+    }
+  }
+}
diff --git a/tools/java/runfiles/BUILD b/tools/java/runfiles/BUILD
index fdd782d..e0487a3 100644
--- a/tools/java/runfiles/BUILD
+++ b/tools/java/runfiles/BUILD
@@ -26,6 +26,8 @@
 filegroup(
     name = "java-srcs",
     srcs = [
+        "AutoBazelRepository.java",
+        "AutoBazelRepositoryProcessor.java",
         "Runfiles.java",
         "Util.java",
     ],
@@ -33,6 +35,22 @@
 
 java_library(
     name = "runfiles",
-    srcs = [":java-srcs"],
+    srcs = [
+        "Runfiles.java",
+        "Util.java",
+    ],
+    exported_plugins = [":auto_bazel_repository_processor"],
     visibility = ["//tools/java/runfiles/testing:__pkg__"],
+    exports = [":auto_bazel_repository"],
+)
+
+java_library(
+    name = "auto_bazel_repository",
+    srcs = ["AutoBazelRepository.java"],
+)
+
+java_plugin(
+    name = "auto_bazel_repository_processor",
+    srcs = ["AutoBazelRepositoryProcessor.java"],
+    processor_class = "com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor",
 )
diff --git a/tools/java/runfiles/BUILD.tools b/tools/java/runfiles/BUILD.tools
index a2ac4f5..70cd870 100644
--- a/tools/java/runfiles/BUILD.tools
+++ b/tools/java/runfiles/BUILD.tools
@@ -4,5 +4,18 @@
         "Runfiles.java",
         "Util.java",
     ],
+    exported_plugins = [":auto_bazel_repository_processor"],
     visibility = ["//visibility:public"],
+    exports = [":auto_bazel_repository"],
+)
+
+java_library(
+    name = "auto_bazel_repository",
+    srcs = ["AutoBazelRepository.java"],
+)
+
+java_plugin(
+    name = "auto_bazel_repository_processor",
+    srcs = ["AutoBazelRepositoryProcessor.java"],
+    processor_class = "com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor",
 )