Update synthetic markers to include a versioning hash.
This renames the synthetic marker too such that it is not visible to
any previous version of the compiler. Going forward any marker with a
non-matching versioning hash is likwise ignored.
Bug: b/227317456
Change-Id: I87e63aba272c3965d259fffdaa23b01039e04a4c
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index 5a2bffc..9f223c7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -390,9 +390,13 @@
public static DexAnnotation createAnnotationSynthesizedClass(
SyntheticKind kind, DexItemFactory dexItemFactory) {
+ DexString versionHash =
+ dexItemFactory.createString(dexItemFactory.getSyntheticNaming().getVersionHash());
DexAnnotationElement kindElement =
new DexAnnotationElement(dexItemFactory.kindString, DexValueInt.create(kind.getId()));
- DexAnnotationElement[] elements = new DexAnnotationElement[] {kindElement};
+ DexAnnotationElement versionHashElement =
+ new DexAnnotationElement(dexItemFactory.versionHashString, new DexValueString(versionHash));
+ DexAnnotationElement[] elements = new DexAnnotationElement[] {kindElement, versionHashElement};
return new DexAnnotation(
VISIBILITY_BUILD,
new DexEncodedAnnotation(dexItemFactory.annotationSynthesizedClass, elements));
@@ -413,17 +417,28 @@
return null;
}
int length = annotation.annotation.elements.length;
- if (length != 1) {
+ if (length != 2) {
return null;
}
- assert factory.kindString.isLessThan(factory.valueString);
+ assert factory.kindString.isLessThan(factory.versionHashString);
DexAnnotationElement kindElement = annotation.annotation.elements[0];
+ DexAnnotationElement versionHashElement = annotation.annotation.elements[1];
if (kindElement.name != factory.kindString) {
return null;
}
if (!kindElement.value.isDexValueInt()) {
return null;
}
+ if (versionHashElement.name != factory.versionHashString) {
+ return null;
+ }
+ if (!versionHashElement.value.isDexValueString()) {
+ return null;
+ }
+ String currentVersionHash = synthetics.getNaming().getVersionHash();
+ if (!currentVersionHash.equals(versionHashElement.value.asDexValueString().toString())) {
+ return null;
+ }
SyntheticKind kind =
synthetics.getNaming().fromId(kindElement.value.asDexValueInt().getValue());
return kind;
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 3e5150c..98dac6d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -338,6 +338,7 @@
public final DexString valueString = createString("value");
public final DexString kindString = createString("kind");
+ public final DexString versionHashString = createString("versionHash");
// Prefix for runtime affecting yet potential class-retained annotations.
public final DexString dalvikAnnotationOptimizationPrefix =
@@ -643,7 +644,7 @@
createStaticallyKnownType("Ldalvik/annotation/SourceDebugExtension;");
public final DexType annotationThrows = createStaticallyKnownType("Ldalvik/annotation/Throws;");
public final DexType annotationSynthesizedClass =
- createStaticallyKnownType("Lcom/android/tools/r8/annotations/SynthesizedClass;");
+ createStaticallyKnownType("Lcom/android/tools/r8/annotations/SynthesizedClassV2;");
public final DexType annotationCovariantReturnType =
createStaticallyKnownType("Ldalvik/annotation/codegen/CovariantReturnType;");
public final DexType annotationCovariantReturnTypes =
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
index ceb9a37..67dbe6a 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
+import java.nio.charset.StandardCharsets;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ByteVector;
import org.objectweb.asm.ClassReader;
@@ -23,33 +24,51 @@
public class SyntheticMarker {
private static final String SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME =
- "com.android.tools.r8.SynthesizedClass";
+ "com.android.tools.r8.SynthesizedClassV2";
public static Attribute getMarkerAttributePrototype(SyntheticNaming syntheticNaming) {
- return new MarkerAttribute(null, syntheticNaming);
+ return new MarkerAttribute(null, null, syntheticNaming);
}
public static void writeMarkerAttribute(
ClassWriter writer, SyntheticKind kind, SyntheticItems syntheticItems) {
- writer.visitAttribute(new MarkerAttribute(kind, syntheticItems.getNaming()));
+ SyntheticNaming naming = syntheticItems.getNaming();
+ writer.visitAttribute(new MarkerAttribute(kind, naming.getVersionHash(), naming));
}
public static SyntheticMarker readMarkerAttribute(Attribute attribute) {
if (attribute instanceof MarkerAttribute) {
MarkerAttribute marker = (MarkerAttribute) attribute;
- return new SyntheticMarker(marker.kind, null);
+ if (marker.versionHash.equals(marker.syntheticNaming.getVersionHash())) {
+ return new SyntheticMarker(marker.kind, null);
+ }
}
return null;
}
+ /**
+ * CF attribute for marking synthetic classes.
+ *
+ * <p>The attribute name is defined by {@code SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME}. The format of
+ * the attribute payload is
+ *
+ * <pre>
+ * u2 syntheticKindId
+ * u2 versionHashLength
+ * u1[versionHashLength] versionHashBytes
+ * </pre>
+ */
private static class MarkerAttribute extends Attribute {
- private SyntheticKind kind;
+ private final SyntheticKind kind;
+ private final String versionHash;
private final SyntheticNaming syntheticNaming;
- public MarkerAttribute(SyntheticKind kind, SyntheticNaming syntheticNaming) {
+ public MarkerAttribute(
+ SyntheticKind kind, String versionHash, SyntheticNaming syntheticNaming) {
super(SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME);
this.kind = kind;
+ this.versionHash = versionHash;
this.syntheticNaming = syntheticNaming;
}
@@ -61,18 +80,29 @@
char[] charBuffer,
int codeAttributeOffset,
Label[] labels) {
- short id = classReader.readShort(offset);
- assert id >= 0;
- SyntheticKind kind = syntheticNaming.fromId(id);
- return new MarkerAttribute(kind, syntheticNaming);
+ short syntheticKindId = classReader.readShort(offset);
+ offset += 2;
+ short versionHashLength = classReader.readShort(offset);
+ offset += 2;
+ byte[] versionHashBytes = new byte[versionHashLength];
+ for (int i = 0; i < versionHashLength; i++) {
+ versionHashBytes[i] = (byte) classReader.readByte(offset++);
+ }
+ assert syntheticKindId >= 0;
+ SyntheticKind kind = syntheticNaming.fromId(syntheticKindId);
+ String versionHash = new String(versionHashBytes, StandardCharsets.UTF_8);
+ return new MarkerAttribute(kind, versionHash, syntheticNaming);
}
@Override
protected ByteVector write(
ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
- ByteVector byteVector = new ByteVector();
assert 0 <= kind.getId() && kind.getId() <= Short.MAX_VALUE;
+ ByteVector byteVector = new ByteVector();
byteVector.putShort(kind.getId());
+ byte[] versionHashBytes = versionHash.getBytes(StandardCharsets.UTF_8);
+ byteVector.putShort(versionHashBytes.length);
+ byteVector.putByteArray(versionHashBytes, 0, versionHashBytes.length);
return byteVector;
}
}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index edc591e..e263786 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.synthesis;
+import com.android.tools.r8.Version;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
@@ -12,16 +13,17 @@
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.structural.Equatable;
import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class SyntheticNaming {
- public SyntheticNaming() {}
-
private KindGenerator generator = new KindGenerator();
// Global synthetics.
@@ -87,7 +89,19 @@
public final SyntheticKind ARRAY_CONVERSION = generator.forSingleMethod(37, "$ArrayConversion");
public final SyntheticKind API_MODEL_OUTLINE = generator.forSingleMethod(32, "ApiModelOutline");
- private final List<SyntheticKind> ALL_KINDS = generator.getAllKinds();
+ private final String versionHash;
+ private final List<SyntheticKind> ALL_KINDS;
+
+ public SyntheticNaming() {
+ generator.hasher.putString(Version.getVersionString(), StandardCharsets.UTF_8);
+ versionHash = generator.hasher.hash().toString();
+ ALL_KINDS = generator.getAllKinds();
+ generator = null;
+ }
+
+ public String getVersionHash() {
+ return versionHash;
+ }
public Collection<SyntheticKind> kinds() {
return ALL_KINDS;
@@ -105,11 +119,13 @@
private static class KindGenerator {
private List<SyntheticKind> kinds = new ArrayList<>();
private IntSet usedIds = new IntArraySet();
+ private Hasher hasher = Hashing.sha256().newHasher();
private SyntheticKind register(SyntheticKind kind) {
if (!usedIds.add(kind.getId())) {
throw new Unreachable("Invalid reuse of synthetic kind id: " + kind.getId());
}
+ kind.hash(hasher);
kinds.add(kind);
return kind;
}
@@ -198,6 +214,13 @@
public abstract boolean isMayOverridesNonProgramType();
+ public final void hash(Hasher hasher) {
+ hasher.putInt(getId());
+ hasher.putString(getDescriptor(), StandardCharsets.UTF_8);
+ internalHash(hasher);
+ }
+
+ public abstract void internalHash(Hasher hasher);
}
private static class SyntheticMethodKind extends SyntheticKind {
@@ -232,6 +255,10 @@
return false;
}
+ @Override
+ public void internalHash(Hasher hasher) {
+ hasher.putString("method", StandardCharsets.UTF_8);
+ }
}
private static class SyntheticClassKind extends SyntheticKind {
@@ -269,6 +296,11 @@
return false;
}
+ @Override
+ public void internalHash(Hasher hasher) {
+ hasher.putString("class", StandardCharsets.UTF_8);
+ hasher.putBoolean(sharable);
+ }
}
private static class SyntheticFixedClassKind extends SyntheticClassKind {
@@ -299,6 +331,11 @@
return mayOverridesNonProgramType;
}
+ @Override
+ public void internalHash(Hasher hasher) {
+ hasher.putString(isGlobal() ? "global" : "fixed", StandardCharsets.UTF_8);
+ hasher.putBoolean(mayOverridesNonProgramType);
+ }
}
private static final String SYNTHETIC_CLASS_SEPARATOR = "$$";
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
new file mode 100644
index 0000000..706ace9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
@@ -0,0 +1,261 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.synthesis;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ByteVector;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+
+@RunWith(Parameterized.class)
+public class SyntheticMarkerCfTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("Hello, world");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withDefaultCfRuntime()
+ .withApiLevel(AndroidApiLevel.B)
+ .enableApiLevelsForCf()
+ .build();
+ }
+
+ public SyntheticMarkerCfTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ /**
+ * Mirror of the initial D8 synthetic marker format.
+ *
+ * <p>The legacy marker just had the synthetic kind id as payload.
+ */
+ private static class SyntheticMarkerV1 extends Attribute {
+ static final SyntheticMarkerV1 PROTO = new SyntheticMarkerV1((short) 0);
+
+ final short kindId;
+
+ public SyntheticMarkerV1(short kindId) {
+ super("com.android.tools.r8.SynthesizedClass");
+ this.kindId = kindId;
+ }
+
+ @Override
+ protected Attribute read(
+ ClassReader classReader,
+ int offset,
+ int length,
+ char[] charBuffer,
+ int codeAttributeOffset,
+ Label[] labels) {
+ short id = classReader.readShort(offset);
+ return new SyntheticMarkerV1(id);
+ }
+
+ @Override
+ protected ByteVector write(
+ ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+ ByteVector byteVector = new ByteVector();
+ byteVector.putShort(kindId);
+ return byteVector;
+ }
+ }
+
+ /**
+ * Format of the current synthetic marker.
+ *
+ * <p>The marker is distinguished by a new attribute type name.
+ *
+ * <p>The payload is the kind id, version hash length and then version hash bytes.
+ */
+ private static class SyntheticMarkerV2 extends Attribute {
+ static final SyntheticMarkerV2 PROTO = new SyntheticMarkerV2((short) 0, null);
+
+ final short kindId;
+ final byte[] versionHash;
+
+ public SyntheticMarkerV2(short kindId, byte[] versionHash) {
+ super("com.android.tools.r8.SynthesizedClassV2");
+ this.versionHash = versionHash;
+ this.kindId = kindId;
+ }
+
+ @Override
+ protected Attribute read(
+ ClassReader classReader,
+ int offset,
+ int length,
+ char[] charBuffer,
+ int codeAttributeOffset,
+ Label[] labels) {
+ short kindId = classReader.readShort(offset);
+ offset += 2;
+ short versionLength = classReader.readShort(offset);
+ offset += 2;
+ byte[] versionBytes = new byte[versionLength];
+ for (int i = 0; i < versionLength; i++) {
+ versionBytes[i] = (byte) classReader.readByte(offset++);
+ }
+ return new SyntheticMarkerV2(kindId, versionBytes);
+ }
+
+ @Override
+ protected ByteVector write(
+ ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+ ByteVector byteVector = new ByteVector();
+ byteVector.putShort(kindId);
+ byteVector.putShort(versionHash.length);
+ byteVector.putByteArray(versionHash, 0, versionHash.length);
+ return byteVector;
+ }
+ }
+
+ private static List<Attribute> readAttributes(byte[] bytes) {
+ List<Attribute> attributes = new ArrayList<>();
+ ClassReader reader = new ClassReader(bytes);
+ reader.accept(
+ new ClassVisitor(InternalOptions.ASM_VERSION) {
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitAttribute(Attribute attribute) {
+ attributes.add(attribute);
+ }
+ },
+ new Attribute[] {SyntheticMarkerV1.PROTO, SyntheticMarkerV2.PROTO},
+ ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
+ return attributes;
+ }
+
+ private byte[] getTestClassWithMarker(Attribute marker) throws IOException {
+ return transformer(TestClass.class)
+ .addClassTransformer(
+ new ClassTransformer() {
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ super.visit(version, access, name, signature, superName, interfaces);
+ super.visitAttribute(marker);
+ }
+ })
+ .transform();
+ }
+
+ /** Test that reads the correct marker from a compilation unit and fails if then manipulated. */
+ @Test
+ public void testInvalidMarkerFailsCompilation() throws Exception {
+ Box<SyntheticMarkerV2> currentCompilerMarker = new Box<>();
+ testForD8(parameters.getBackend())
+ .addProgramClasses(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .setIntermediate(true)
+ .setProgramConsumer(
+ new ClassFileConsumer() {
+ private final List<Attribute> attributes = new ArrayList<>();
+
+ @Override
+ public void accept(ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+ attributes.addAll(readAttributes(data.copyByteData()));
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ assertEquals(1, attributes.size());
+ assertEquals(SyntheticMarkerV2.PROTO.type, attributes.get(0).type);
+ currentCompilerMarker.set((SyntheticMarkerV2) attributes.get(0));
+ }
+ })
+ .compile()
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+
+ // Test that if a "current" marker with invalid content is given to the compiler it will
+ // cause a failure. This test ensures that we can witness the markers being ignored in the
+ // tests below.
+ D8TestBuilder builder =
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addProgramClassFileData(
+ getTestClassWithMarker(
+ new SyntheticMarkerV2(
+ Short.MAX_VALUE, currentCompilerMarker.get().versionHash)));
+ assertThrows(CompilationFailedException.class, builder::compile);
+ }
+
+ @Test
+ public void testIgnoreV1Markers() throws Exception {
+ // Test that inputs with a legacy marker will be ignored.
+ // We do so by injecting an old marker and put in a non-valid ID which would cause the compiler
+ // to fail if it was read.
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addProgramClassFileData(getTestClassWithMarker(new SyntheticMarkerV1(Short.MAX_VALUE)))
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testIgnorePreviousV2Markers() throws Exception {
+ // Test that inputs with a V2 marker from a previous compiler version are ignored.
+ // We do so by injecting an old marker and put in a non-valid ID which would cause the compiler
+ // to fail if it was read.
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addProgramClassFileData(
+ getTestClassWithMarker(new SyntheticMarkerV2(Short.MAX_VALUE, new byte[0])))
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ public static class TestClass {
+
+ public static void run(Runnable r) {
+ r.run();
+ }
+
+ public static void main(String[] args) {
+ run(() -> System.out.println("Hello, world"));
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerDexTest.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerDexTest.java
new file mode 100644
index 0000000..5b00775
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerDexTest.java
@@ -0,0 +1,89 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.synthesis;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexEncodedAnnotation;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SyntheticMarkerDexTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("Hello, world");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultDexRuntime().withApiLevel(AndroidApiLevel.B).build();
+ }
+
+ public SyntheticMarkerDexTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ Path out =
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .setIntermediate(true)
+ .addProgramClasses(TestClass.class)
+ .compile()
+ .inspect(this::checkSyntheticClassIsMarked)
+ .writeToZip();
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addProgramFiles(out)
+ // Use intermediate again to preserve synthetics.
+ .setIntermediate(true)
+ // Disable desugaring so we are sure the lambda synthetic is created in the first round.
+ .disableDesugaring()
+ .compile()
+ .inspect(this::checkSyntheticClassIsMarked)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ private void checkSyntheticClassIsMarked(CodeInspector inspector) {
+ // Compilation gives rise to the main class plus one lambda.
+ assertEquals(2, inspector.allClasses().size());
+ inspector.forAllClasses(
+ clazz -> {
+ if (!clazz.getFinalReference().equals(Reference.classFromClass(TestClass.class))) {
+ // This should be the lambda class.
+ DexAnnotation[] annotations = clazz.getDexProgramClass().annotations().annotations;
+ assertEquals(1, annotations.length);
+ DexEncodedAnnotation annotation = annotations[0].annotation;
+ assertEquals(2, annotation.elements.length);
+ assertEquals(
+ "com.android.tools.r8.annotations.SynthesizedClassV2",
+ annotation.type.toSourceString());
+ }
+ });
+ }
+
+ public static class TestClass {
+
+ public static void run(Runnable r) {
+ r.run();
+ }
+
+ public static void main(String[] args) {
+ run(() -> System.out.println("Hello, world"));
+ }
+ }
+}