Collect the contexts of D8 compiler-synthesized classes.

This completes steps 2 and 3 in b/241351268#comment9
towards resolving https://github.com/bazelbuild/bazel/issues/16368

PiperOrigin-RevId: 530238344
Change-Id: I569bf8e3bfa81f7005f3e9b79338d5a5b868e339
diff --git a/src/test/java/com/google/devtools/build/android/r8/BUILD b/src/test/java/com/google/devtools/build/android/r8/BUILD
index 940b22b..701fcf9 100644
--- a/src/test/java/com/google/devtools/build/android/r8/BUILD
+++ b/src/test/java/com/google/devtools/build/android/r8/BUILD
@@ -51,6 +51,7 @@
         ":arithmetic",
         ":barray",
         ":naming001",
+        ":testdata_lambda_desugared.jar",
         ":twosimpleclasses",
     ],
     jvm_flags = [
@@ -58,6 +59,7 @@
         "-DCompatDexBuilderTests.naming001=$(location :naming001)",
         "-DCompatDxTests.arithmetic=$(location :arithmetic)",
         "-DCompatDxTests.barray=$(location :barray)",
+        "-DCompatDexBuilderTests.lambda=$(location :testdata_lambda_desugared.jar)",
     ],
     runtime_deps = [
         ":tests",
@@ -103,3 +105,21 @@
         "-target 8",
     ],
 )
+
+java_library(
+    name = "testdata_lambda",
+    srcs = glob(["testdata/lambda/*.java"]),
+)
+
+genrule(
+    name = "desugar_testdata_lambda",
+    srcs = [
+        ":testdata_lambda",
+        "@bazel_tools//tools/android:android_jar",
+    ],
+    outs = ["testdata_lambda_desugared.jar"],
+    cmd = "$(location //src/tools/android/java/com/google/devtools/build/android/r8:desugar) " +
+          "-i $(location :testdata_lambda) -o $@ " +
+          "--bootclasspath_entry $(location @bazel_tools//tools/android:android_jar)",
+    tools = ["//src/tools/android/java/com/google/devtools/build/android/r8:desugar"],
+)
diff --git a/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java b/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java
index 158200b..8c88cae 100644
--- a/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java
@@ -21,13 +21,15 @@
 import com.android.tools.r8.OutputMode;
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.common.options.OptionsParsingException;
+import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
+import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import org.junit.Rule;
 import org.junit.Test;
@@ -45,7 +47,7 @@
       throws IOException, InterruptedException, ExecutionException, OptionsParsingException {
     // Random set of classes from the R8 example test directory naming001.
     final String inputJar = System.getProperty("CompatDexBuilderTests.naming001");
-    final List<String> classNames =
+    final ImmutableList<String> classNames =
         ImmutableList.of(
             "A",
             "B",
@@ -87,6 +89,40 @@
   }
 
   @Test
+  public void compileWithSyntheticLambdas() throws Exception {
+    final String contextName = "com/google/devtools/build/android/r8/testdata/lambda/Lambda";
+    final String inputJar = System.getProperty("CompatDexBuilderTests.lambda");
+    final Path outputZip = temp.getRoot().toPath().resolve("out.zip");
+    CompatDexBuilder.main(
+        new String[] {"--input_jar", inputJar, "--output_zip", outputZip.toString()});
+    assertThat(Files.exists(outputZip)).isTrue();
+
+    try (ZipFile zipFile = new ZipFile(outputZip.toFile(), UTF_8)) {
+      assertThat(zipFile.getEntry(contextName + ".class.dex")).isNotNull();
+      ZipEntry entry = zipFile.getEntry("META-INF/synthetic-contexts.map");
+      assertThat(entry).isNotNull();
+      try (BufferedReader reader =
+          new BufferedReader(new InputStreamReader(zipFile.getInputStream(entry), UTF_8))) {
+        String line = reader.readLine();
+        assertThat(line).isNotNull();
+        // Format of mapping is: <synthetic-binary-name>;<context-binary-name>\n
+        int sep = line.indexOf(';');
+        String syntheticNameInMap = line.substring(0, sep);
+        String contextNameInMap = line.substring(sep + 1);
+        // The synthetic will be prefixed by the context type. This checks the synthetic name
+        // is larger than the context to avoid hardcoding the synthetic names, which may change.
+        assertThat(syntheticNameInMap).startsWith(contextName);
+        assertThat(syntheticNameInMap).isNotEqualTo(contextName);
+        // Check expected context.
+        assertThat(contextNameInMap).isEqualTo(contextName);
+        // Only one synthetic and its context should be present.
+        line = reader.readLine();
+        assertThat(line).isNull();
+      }
+    }
+  }
+
+  @Test
   public void compileTwoClassesAndRun() throws Exception {
     // Run CompatDexBuilder on dexMergeSample.jar
     final String inputJar = System.getProperty("CompatDexBuilderTests.twosimpleclasses");
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/lambda/Lambda.java b/src/test/java/com/google/devtools/build/android/r8/testdata/lambda/Lambda.java
new file mode 100644
index 0000000..2fa5fed
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/lambda/Lambda.java
@@ -0,0 +1,30 @@
+// Copyright 2023 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.android.r8.testdata.lambda;
+
+import java.util.function.Supplier;
+
+/** Test class */
+public final class Lambda {
+
+  private Lambda() {}
+
+  private static <T> T foo(Supplier<T> fn) {
+    return fn.get();
+  }
+
+  public static void main(String[] args) {
+    String unused = foo(() -> "Hello, world!");
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/BUILD b/src/tools/android/java/com/google/devtools/build/android/r8/BUILD
index d99e969..55e6289 100644
--- a/src/tools/android/java/com/google/devtools/build/android/r8/BUILD
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/BUILD
@@ -89,3 +89,25 @@
         "//src/main/java/com/google/devtools/build/lib/worker:work_request_handlers",
     ],
 )
+
+java_binary(
+    name = "desugar",
+    jvm_flags = [
+        # b/71513487
+        "-XX:+TieredCompilation",
+        "-XX:TieredStopAtLevel=1",
+        "-Xms8g",
+        "-Xmx8g",
+        # b/172508621
+        "-Dcom.android.tools.r8.sortMethodsOnCfWriting",
+        "-Dcom.android.tools.r8.allowAllDesugaredInput",
+        "-Dcom.android.tools.r8.noCfMarkerForDesugaredCode",
+        "-Dcom.android.tools.r8.lambdaClassFieldsNotFinal",
+        "-Dcom.android.tools.r8.createSingletonsForStatelessLambdas",
+    ],
+    main_class = "com.google.devtools.build.android.r8.Desugar",
+    visibility = ["//src/test/java/com/google/devtools/build/android/r8:__pkg__"],
+    runtime_deps = [
+        ":r8",
+    ],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java b/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java
index 0de6e36..491f99b 100644
--- a/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java
@@ -24,8 +24,11 @@
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.SyntheticInfoConsumer;
+import com.android.tools.r8.SyntheticInfoConsumerData;
 import com.android.tools.r8.origin.ArchiveEntryOrigin;
 import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.references.ClassReference;
 import com.google.auto.value.AutoValue;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
@@ -67,8 +70,47 @@
 public class CompatDexBuilder {
   private static final long ONE_MEG = 1024 * 1024;
 
+  private static class ContextConsumer implements SyntheticInfoConsumer {
+
+    // After compilation this will be non-null iff the compiled class is a D8 synthesized class.
+    ClassReference sythesizedPrimaryClass = null;
+
+    // If the above is non-null then this will be the non-synthesized context class that caused
+    // D8 to synthesize the above class.
+    ClassReference contextOfSynthesizedClass = null;
+
+    @Nullable
+    String getContextMapping() {
+      if (sythesizedPrimaryClass != null) {
+        return sythesizedPrimaryClass.getBinaryName()
+            + ";"
+            + contextOfSynthesizedClass.getBinaryName();
+      }
+      return null;
+    }
+
+    @Override
+    public synchronized void acceptSyntheticInfo(SyntheticInfoConsumerData data) {
+      verify(
+          sythesizedPrimaryClass == null || sythesizedPrimaryClass.equals(data.getSyntheticClass()),
+          "The single input classfile should ensure this has one value.");
+      verify(
+          contextOfSynthesizedClass == null
+              || contextOfSynthesizedClass.equals(data.getSynthesizingContextClass()),
+          "The single input classfile should ensure this has one value.");
+      sythesizedPrimaryClass = data.getSyntheticClass();
+      contextOfSynthesizedClass = data.getSynthesizingContextClass();
+    }
+
+    @Override
+    public void finished() {
+      // Do nothing.
+    }
+  }
+
   private static class DexConsumer implements DexIndexedConsumer {
 
+    final ContextConsumer contextConsumer = new ContextConsumer();
     byte[] bytes;
 
     @Override
@@ -86,6 +128,10 @@
       this.bytes = byteCode;
     }
 
+    ContextConsumer getContextConsumer() {
+      return contextConsumer;
+    }
+
     @Override
     public void finished(DiagnosticsHandler handler) {
       // Do nothing.
@@ -269,10 +315,23 @@
                           minSdkVersion,
                           executor)));
         }
+        StringBuilder contextMappingBuilder = new StringBuilder();
         for (int i = 0; i < futures.size(); i++) {
           ZipEntry entry = toDex.get(i);
           DexConsumer consumer = futures.get(i).get();
           ZipUtils.addEntry(entry.getName() + ".dex", consumer.getBytes(), ZipEntry.STORED, out);
+          String mapping = consumer.getContextConsumer().getContextMapping();
+          if (mapping != null) {
+            contextMappingBuilder.append(mapping).append('\n');
+          }
+        }
+        String contextMapping = contextMappingBuilder.toString();
+        if (!contextMapping.isEmpty()) {
+          ZipUtils.addEntry(
+              "META-INF/synthetic-contexts.map",
+              contextMapping.getBytes(UTF_8),
+              ZipEntry.STORED,
+              out);
         }
       }
     } finally {
@@ -292,6 +351,7 @@
     D8Command.Builder builder = D8Command.builder();
     builder
         .setProgramConsumer(consumer)
+        .setSyntheticInfoConsumer(consumer.getContextConsumer())
         .setMode(mode)
         .setMinApiLevel(minSdkVersion)
         .setDisableDesugaring(true)