Fix handling of repackaged transitive classes in jdeps

Turbine supports running with only direct dependencies on the classpath (see
https://github.com/google/turbine/commit/d1509927c68b994ecb9eb95a8ae8478da9f04ed4).
In this mode it repackages transitive supertypes of classes referenced in the
compilation, and saves them in the output jar under `META-INF/TRANSITIVE`.

When turbine records classes that were used in the compilation for `jdeps`
output, it was reporting the jar that it loaded a repackaged transitive class
from, instead of the jar where that repackaged transitive class was originally
found.

This change adds a `TurbineTransitiveJar` class file attribute to the
repackaged classes that records the path of the jar file they were originally
seen in, and updates the `jdeps` logic to report that original path instead of
the jar where the repackaged class was observed.

PiperOrigin-RevId: 380075237
diff --git a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
index 44b64bb..01da961 100644
--- a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
+++ b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
@@ -629,6 +629,10 @@
 
   /** The jar file the symbol was loaded from. */
   public @Nullable String jarFile() {
+    String transitiveJar = classFile.get().transitiveJar();
+    if (transitiveJar != null) {
+      return transitiveJar;
+    }
     return jarFile;
   }
 
diff --git a/java/com/google/turbine/bytecode/Attribute.java b/java/com/google/turbine/bytecode/Attribute.java
index 29efb60..7b415a7 100644
--- a/java/com/google/turbine/bytecode/Attribute.java
+++ b/java/com/google/turbine/bytecode/Attribute.java
@@ -41,7 +41,8 @@
     RUNTIME_VISIBLE_TYPE_ANNOTATIONS("RuntimeVisibleTypeAnnotations"),
     RUNTIME_INVISIBLE_TYPE_ANNOTATIONS("RuntimeInvisibleTypeAnnotations"),
     METHOD_PARAMETERS("MethodParameters"),
-    MODULE("Module");
+    MODULE("Module"),
+    TURBINE_TRANSITIVE_JAR("TurbineTransitiveJar");
 
     private final String signature;
 
@@ -309,4 +310,19 @@
       return module;
     }
   }
+
+  /** A custom attribute for recording the original jar of repackaged transitive classes. */
+  class TurbineTransitiveJar implements Attribute {
+
+    final String transitiveJar;
+
+    public TurbineTransitiveJar(String transitiveJar) {
+      this.transitiveJar = transitiveJar;
+    }
+
+    @Override
+    public Kind kind() {
+      return Kind.TURBINE_TRANSITIVE_JAR;
+    }
+  }
 }
diff --git a/java/com/google/turbine/bytecode/AttributeWriter.java b/java/com/google/turbine/bytecode/AttributeWriter.java
index c5ffd16..84ca55f 100644
--- a/java/com/google/turbine/bytecode/AttributeWriter.java
+++ b/java/com/google/turbine/bytecode/AttributeWriter.java
@@ -24,6 +24,7 @@
 import com.google.turbine.bytecode.Attribute.InnerClasses;
 import com.google.turbine.bytecode.Attribute.MethodParameters;
 import com.google.turbine.bytecode.Attribute.Signature;
+import com.google.turbine.bytecode.Attribute.TurbineTransitiveJar;
 import com.google.turbine.bytecode.Attribute.TypeAnnotations;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo;
 import com.google.turbine.bytecode.ClassFile.MethodInfo.ParameterInfo;
@@ -87,6 +88,9 @@
       case MODULE:
         writeModule((Attribute.Module) attribute);
         break;
+      case TURBINE_TRANSITIVE_JAR:
+        writeTurbineTransitiveJar((Attribute.TurbineTransitiveJar) attribute);
+        break;
     }
   }
 
@@ -266,4 +270,10 @@
     output.writeInt(data.length);
     output.write(data);
   }
+
+  private void writeTurbineTransitiveJar(TurbineTransitiveJar attribute) {
+    output.writeShort(pool.utf8(attribute.kind().signature()));
+    output.writeInt(2);
+    output.writeShort(pool.utf8(attribute.transitiveJar));
+  }
 }
diff --git a/java/com/google/turbine/bytecode/ClassFile.java b/java/com/google/turbine/bytecode/ClassFile.java
index 8ee2aac..e979edc 100644
--- a/java/com/google/turbine/bytecode/ClassFile.java
+++ b/java/com/google/turbine/bytecode/ClassFile.java
@@ -42,6 +42,7 @@
   private final List<InnerClass> innerClasses;
   private final ImmutableList<TypeAnnotationInfo> typeAnnotations;
   @Nullable private final ModuleInfo module;
+  @Nullable private final String transitiveJar;
 
   public ClassFile(
       int access,
@@ -54,7 +55,8 @@
       List<AnnotationInfo> annotations,
       List<InnerClass> innerClasses,
       ImmutableList<TypeAnnotationInfo> typeAnnotations,
-      @Nullable ModuleInfo module) {
+      @Nullable ModuleInfo module,
+      @Nullable String transitiveJar) {
     this.access = access;
     this.name = name;
     this.signature = signature;
@@ -66,6 +68,7 @@
     this.innerClasses = innerClasses;
     this.typeAnnotations = typeAnnotations;
     this.module = module;
+    this.transitiveJar = transitiveJar;
   }
 
   /** Class access and property flags. */
@@ -124,6 +127,12 @@
     return module;
   }
 
+  /** The original jar of a repackaged transitive class. */
+  @Nullable
+  public String transitiveJar() {
+    return transitiveJar;
+  }
+
   /** The contents of a JVMS §4.5 field_info structure. */
   public static class FieldInfo {
 
diff --git a/java/com/google/turbine/bytecode/ClassReader.java b/java/com/google/turbine/bytecode/ClassReader.java
index 9c79b42..ac8b1e1 100644
--- a/java/com/google/turbine/bytecode/ClassReader.java
+++ b/java/com/google/turbine/bytecode/ClassReader.java
@@ -106,6 +106,7 @@
     List<ClassFile.InnerClass> innerclasses = ImmutableList.of();
     ImmutableList.Builder<ClassFile.AnnotationInfo> annotations = ImmutableList.builder();
     ClassFile.ModuleInfo module = null;
+    String transitiveJar = null;
     int attributesCount = reader.u2();
     for (int j = 0; j < attributesCount; j++) {
       int attributeNameIndex = reader.u2();
@@ -124,6 +125,9 @@
         case "Module":
           module = readModule(constantPool);
           break;
+        case "TurbineTransitiveJar":
+          transitiveJar = readTurbineTransitiveJar(constantPool);
+          break;
         default:
           reader.skip(reader.u4());
           break;
@@ -141,7 +145,8 @@
         annotations.build(),
         innerclasses,
         ImmutableList.of(),
-        module);
+        module,
+        transitiveJar);
   }
 
   /** Reads a JVMS 4.7.9 Signature attribute. */
@@ -509,4 +514,9 @@
     }
     return fields;
   }
+
+  private String readTurbineTransitiveJar(ConstantPoolReader constantPool) {
+    reader.u4(); // length
+    return constantPool.utf8(reader.u2());
+  }
 }
diff --git a/java/com/google/turbine/bytecode/LowerAttributes.java b/java/com/google/turbine/bytecode/LowerAttributes.java
index 42fcf5c..5ae42af 100644
--- a/java/com/google/turbine/bytecode/LowerAttributes.java
+++ b/java/com/google/turbine/bytecode/LowerAttributes.java
@@ -45,6 +45,9 @@
     if (classfile.module() != null) {
       attributes.add(new Attribute.Module(classfile.module()));
     }
+    if (classfile.transitiveJar() != null) {
+      attributes.add(new Attribute.TurbineTransitiveJar(classfile.transitiveJar()));
+    }
     return attributes;
   }
 
diff --git a/java/com/google/turbine/deps/Transitive.java b/java/com/google/turbine/deps/Transitive.java
index a1ddc83..75d23f6 100644
--- a/java/com/google/turbine/deps/Transitive.java
+++ b/java/com/google/turbine/deps/Transitive.java
@@ -33,6 +33,7 @@
 import com.google.turbine.model.TurbineFlag;
 import java.util.LinkedHashSet;
 import java.util.Set;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * Collects the minimal compile-time API for symbols in the supertype closure of compiled classes.
@@ -54,7 +55,8 @@
         // don't export symbols loaded from the bootclasspath
         continue;
       }
-      transitive.put(sym.binaryName(), ClassWriter.writeClass(trimClass(info.classFile())));
+      transitive.put(
+          sym.binaryName(), ClassWriter.writeClass(trimClass(info.classFile(), info.jarFile())));
     }
     return transitive.build();
   }
@@ -62,7 +64,7 @@
   /**
    * Removes information from repackaged classes that will not be needed by upstream compilations.
    */
-  public static ClassFile trimClass(ClassFile cf) {
+  public static ClassFile trimClass(ClassFile cf, @Nullable String jarFile) {
     // drop non-constant fields
     ImmutableList.Builder<FieldInfo> fields = ImmutableList.builder();
     for (FieldInfo f : cf.fields()) {
@@ -80,6 +82,12 @@
         innerClasses.add(i);
       }
     }
+    // Include the original jar file name when repackaging transitive deps. If the same transitive
+    // dep is repackaged more than once, keep the original name.
+    String transitiveJar = cf.transitiveJar();
+    if (transitiveJar == null) {
+      transitiveJar = jarFile;
+    }
     return new ClassFile(
         cf.access(),
         cf.name(),
@@ -96,7 +104,8 @@
         cf.annotations(),
         innerClasses.build(),
         cf.typeAnnotations(),
-        /* module= */ null);
+        /* module= */ null,
+        /* transitiveJar = */ transitiveJar);
   }
 
   private static Set<ClassSymbol> superClosure(BindingResult bound) {
diff --git a/java/com/google/turbine/lower/Lower.java b/java/com/google/turbine/lower/Lower.java
index 0f7bb90..971bbe4 100644
--- a/java/com/google/turbine/lower/Lower.java
+++ b/java/com/google/turbine/lower/Lower.java
@@ -185,7 +185,8 @@
             annotations,
             innerClasses.build(),
             /* typeAnnotations= */ ImmutableList.of(),
-            moduleInfo);
+            moduleInfo,
+            /* transitiveJar= */ null);
     symbols.addAll(sig.classes);
     return ClassWriter.writeClass(classfile);
   }
@@ -279,7 +280,8 @@
             annotations,
             inners,
             typeAnnotations,
-            /* module= */ null);
+            /* module= */ null,
+            /* transitiveJar= */ null);
 
     symbols.addAll(sig.classes);
 
diff --git a/javatests/com/google/turbine/bytecode/ClassReaderTest.java b/javatests/com/google/turbine/bytecode/ClassReaderTest.java
index 02b5b56..9a9fdb1 100644
--- a/javatests/com/google/turbine/bytecode/ClassReaderTest.java
+++ b/javatests/com/google/turbine/bytecode/ClassReaderTest.java
@@ -35,6 +35,8 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ByteVector;
 import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.ModuleVisitor;
@@ -336,4 +338,28 @@
     assertThat(p2.descriptor()).isEqualTo("p2");
     assertThat(p2.implDescriptors()).containsExactly("p2i1", "p2i2", "p2i3");
   }
+
+  @Test
+  public void transitiveJar() {
+    ClassWriter cw = new ClassWriter(0);
+    cw.visit(
+        52,
+        Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER,
+        "Hello",
+        null,
+        "java/lang/Object",
+        null);
+    cw.visitAttribute(
+        new Attribute("TurbineTransitiveJar") {
+          @Override
+          protected ByteVector write(
+              ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+            ByteVector result = new ByteVector();
+            result.putShort(classWriter.newUTF8("path/to/transitive.jar"));
+            return result;
+          }
+        });
+    ClassFile cf = ClassReader.read(null, cw.toByteArray());
+    assertThat(cf.transitiveJar()).isEqualTo("path/to/transitive.jar");
+  }
 }
diff --git a/javatests/com/google/turbine/deps/TransitiveTest.java b/javatests/com/google/turbine/deps/TransitiveTest.java
index d35f87d..f08e899 100644
--- a/javatests/com/google/turbine/deps/TransitiveTest.java
+++ b/javatests/com/google/turbine/deps/TransitiveTest.java
@@ -17,19 +17,26 @@
 package com.google.turbine.deps;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.turbine.testing.TestClassPaths.optionsWithBootclasspath;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.io.ByteStreams;
+import com.google.protobuf.ExtensionRegistry;
 import com.google.turbine.bytecode.ClassFile;
 import com.google.turbine.bytecode.ClassFile.InnerClass;
 import com.google.turbine.bytecode.ClassReader;
 import com.google.turbine.main.Main;
+import com.google.turbine.proto.DepsProto;
+import com.google.turbine.proto.DepsProto.Dependency.Kind;
+import java.io.BufferedInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -74,7 +81,7 @@
     }
   }
 
-  private Map<String, byte[]> readJar(Path libb) throws IOException {
+  private static Map<String, byte[]> readJar(Path libb) throws IOException {
     Map<String, byte[]> jarEntries = new LinkedHashMap<>();
     try (JarFile jf = new JarFile(libb.toFile())) {
       Enumeration<JarEntry> entries = jf.entries();
@@ -137,11 +144,19 @@
                 .methods())
         .hasSize(1);
 
+    // When a.A is repackaged as a transitive class in libb, its 'transitive jar' attribute
+    // should record the path to the original liba jar.
+    assertThat(a.transitiveJar()).isEqualTo(liba.toString());
+    // The transitive jar attribute is only set for transitive classes, not e.g. b.B in libb:
+    ClassFile b = ClassReader.read(null, readJar(libb).get("b/B.class"));
+    assertThat(b.transitiveJar()).isNull();
+
     // A class that references members of the transitive supertype A by simple name
     // compiles cleanly against the repackaged version of A.
     // Explicitly use turbine; javac-turbine doesn't support direct-classpath compilations.
 
     Path libc = temporaryFolder.newFolder().toPath().resolve("out.jar");
+    Path libcDeps = temporaryFolder.newFolder().toPath().resolve("out.jdeps");
     ImmutableList<String> sources =
         new SourceBuilder()
                 .addSourceLines(
@@ -161,6 +176,7 @@
             .setClassPath(
                 ImmutableList.of(libb).stream().map(Path::toString).collect(toImmutableList()))
             .setOutput(libc.toString())
+            .setOutputDeps(libcDeps.toString())
             .build());
 
     assertThat(readJar(libc).keySet())
@@ -170,6 +186,20 @@
             "META-INF/TRANSITIVE/a/A.class",
             "META-INF/TRANSITIVE/a/A$Anno.class",
             "META-INF/TRANSITIVE/a/A$Inner.class");
+
+    // liba is recorded as an explicit dep, even thought it's only present as a transitive class
+    // repackaged in lib
+    assertThat(readDeps(libcDeps))
+        .containsExactly(liba.toString(), Kind.EXPLICIT, libb.toString(), Kind.EXPLICIT);
+  }
+
+  private static ImmutableMap<String, Kind> readDeps(Path libcDeps) throws IOException {
+    DepsProto.Dependencies.Builder deps = DepsProto.Dependencies.newBuilder();
+    try (InputStream is = new BufferedInputStream(Files.newInputStream(libcDeps))) {
+      deps.mergeFrom(is, ExtensionRegistry.getEmptyRegistry());
+    }
+    return deps.getDependencyList().stream()
+        .collect(toImmutableMap(d -> d.getPath(), d -> d.getKind()));
   }
 
   @Test