Account for removed constructors in the final round of class merging
Bug: b/276385221
Change-Id: I126634174f887ef6898cc3b009ef52b70c435035
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
index bba2edd..9117bcc 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
@@ -39,6 +39,34 @@
AppView<? extends AppInfoWithClassHierarchy> appView,
IRCodeProvider codeProvider,
MergeGroup group,
+ InstanceInitializer instanceInitializer) {
+ if (instanceInitializer.isAbsent()) {
+ InstanceInitializerDescription.Builder builder =
+ InstanceInitializerDescription.builder(appView, instanceInitializer.getReference());
+ DexMethod invokedConstructor =
+ instanceInitializer
+ .getReference()
+ .withHolder(group.getSuperType(), appView.dexItemFactory());
+ List<InstanceFieldInitializationInfo> invokedConstructorArguments = new ArrayList<>();
+ for (int argumentIndex = 1;
+ argumentIndex < invokedConstructor.getNumberOfArguments(false);
+ argumentIndex++) {
+ invokedConstructorArguments.add(
+ appView
+ .instanceFieldInitializationInfoFactory()
+ .createArgumentInitializationInfo(argumentIndex));
+ }
+ builder.addInvokeConstructor(invokedConstructor, invokedConstructorArguments);
+ return builder.build();
+ } else {
+ return analyze(appView, codeProvider, group, instanceInitializer.asPresent().getMethod());
+ }
+ }
+
+ public static InstanceInitializerDescription analyze(
+ AppView<? extends AppInfoWithClassHierarchy> appView,
+ IRCodeProvider codeProvider,
+ MergeGroup group,
ProgramMethod instanceInitializer) {
InstanceInitializerDescription.Builder builder =
InstanceInitializerDescription.builder(appView, instanceInitializer);
@@ -179,4 +207,65 @@
private static InstanceInitializerDescription invalid() {
return null;
}
+
+ public abstract static class InstanceInitializer {
+
+ public abstract DexMethod getReference();
+
+ public abstract boolean isAbsent();
+
+ public abstract PresentInstanceInitializer asPresent();
+ }
+
+ public static class AbsentInstanceInitializer extends InstanceInitializer {
+
+ private final DexMethod methodReference;
+
+ public AbsentInstanceInitializer(DexMethod methodReference) {
+ this.methodReference = methodReference;
+ }
+
+ @Override
+ public DexMethod getReference() {
+ return methodReference;
+ }
+
+ @Override
+ public boolean isAbsent() {
+ return true;
+ }
+
+ @Override
+ public PresentInstanceInitializer asPresent() {
+ return null;
+ }
+ }
+
+ public static class PresentInstanceInitializer extends InstanceInitializer {
+
+ private final ProgramMethod method;
+
+ public PresentInstanceInitializer(ProgramMethod method) {
+ this.method = method;
+ }
+
+ public ProgramMethod getMethod() {
+ return method;
+ }
+
+ @Override
+ public DexMethod getReference() {
+ return method.getReference();
+ }
+
+ @Override
+ public boolean isAbsent() {
+ return false;
+ }
+
+ @Override
+ public PresentInstanceInitializer asPresent() {
+ return this;
+ }
+ }
}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
index 894c017..52a00c1 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
@@ -57,6 +57,11 @@
}
public static Builder builder(
+ AppView<? extends AppInfoWithClassHierarchy> appView, DexMethod instanceInitializer) {
+ return new Builder(appView.dexItemFactory(), instanceInitializer);
+ }
+
+ public static Builder builder(
AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod instanceInitializer) {
return new Builder(appView.dexItemFactory(), instanceInitializer);
}
@@ -120,15 +125,19 @@
private DexMethod parentConstructor;
private List<InstanceFieldInitializationInfo> parentConstructorArguments;
- Builder(DexItemFactory dexItemFactory, ProgramMethod method) {
+ Builder(DexItemFactory dexItemFactory, DexMethod methodReference) {
this.dexItemFactory = dexItemFactory;
this.relaxedParameters =
- method
+ methodReference
.getParameters()
.map(
parameter -> parameter.isPrimitiveType() ? parameter : dexItemFactory.objectType);
}
+ Builder(DexItemFactory dexItemFactory, ProgramMethod method) {
+ this(dexItemFactory, method.getReference());
+ }
+
public void addInstancePut(DexField field, InstanceFieldInitializationInfo value) {
if (parentConstructor == null) {
instanceFieldAssignmentsPre.put(field, value);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
index deb0e02..294d87d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
@@ -282,8 +282,7 @@
MapUtils.newIdentityHashMap(
builder ->
Iterables.filter(groups, MergeGroup::isClassGroup)
- .forEach(
- group -> group.forEach(clazz -> builder.accept(clazz.getType(), group))));
+ .forEach(group -> group.forEach(clazz -> builder.put(clazz.getType(), group))));
// Copy the map from classes to their inherited default methods.
Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
index 02b6bbd..4c07501 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
@@ -4,38 +4,47 @@
package com.android.tools.r8.horizontalclassmerging.policies;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexMethodSignature;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.MethodAccessInfoCollection;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.horizontalclassmerging.ClassInstanceFieldsMerger;
import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
import com.android.tools.r8.horizontalclassmerging.IRCodeProvider;
import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis;
+import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis.AbsentInstanceInitializer;
+import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis.InstanceInitializer;
+import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis.PresentInstanceInitializer;
import com.android.tools.r8.horizontalclassmerging.InstanceInitializerDescription;
import com.android.tools.r8.horizontalclassmerging.MergeGroup;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
+import com.android.tools.r8.utils.IterableUtils;
import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.MapUtils;
import com.android.tools.r8.utils.SetUtils;
import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
-import com.android.tools.r8.utils.collections.ProgramMethodMap;
-import com.google.common.collect.Iterators;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ExecutorService;
import java.util.function.Function;
/**
@@ -46,7 +55,8 @@
* <p>This policy requires that all instance initializers with the same signature (relaxed, by
* converting references types to java.lang.Object) have the same behavior.
*/
-public class NoInstanceInitializerMerging extends MultiClassPolicy {
+public class NoInstanceInitializerMerging
+ extends MultiClassPolicyWithPreprocessing<Map<DexProgramClass, Set<DexMethod>>> {
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final IRCodeProvider codeProvider;
@@ -61,7 +71,53 @@
}
@Override
- public Collection<MergeGroup> apply(MergeGroup group) {
+ public Map<DexProgramClass, Set<DexMethod>> preprocess(
+ Collection<MergeGroup> groups, ExecutorService executorService) {
+ if (!appView.options().canHaveNonReboundConstructorInvoke()) {
+ return Collections.emptyMap();
+ }
+
+ if (appView.hasLiveness()) {
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ MethodAccessInfoCollection methodAccessInfoCollection =
+ appView.appInfoWithLiveness().getMethodAccessInfoCollection();
+
+ // Compute a mapping for the merge candidates to efficiently determine if a given type is a
+ // merge candidate.
+ Map<DexType, DexProgramClass> mergeCandidates =
+ MapUtils.newImmutableMap(
+ builder ->
+ IterableUtils.flatten(groups)
+ .forEach(
+ mergeCandidate -> builder.put(mergeCandidate.getType(), mergeCandidate)));
+
+ // Compute a mapping from merge candidates to the constructors that have been removed by
+ // constructor shrinking.
+ return MapUtils.newIdentityHashMap(
+ builder ->
+ methodAccessInfoCollection.forEachDirectInvoke(
+ (method, contexts) -> {
+ DexProgramClass mergeCandidateHolder =
+ mergeCandidates.get(method.getHolderType());
+ if (mergeCandidateHolder != null
+ && method.isInstanceInitializer(dexItemFactory)
+ && mergeCandidateHolder.getMethodCollection().getMethod(method) == null) {
+ builder
+ .computeIfAbsent(
+ mergeCandidateHolder, ignoreKey(Sets::newIdentityHashSet))
+ .add(method);
+ }
+ }));
+ }
+
+ // Constructor shrinking is disabled when shrinking is disabled.
+ assert !appView.options().isShrinking();
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Collection<MergeGroup> apply(
+ MergeGroup group, Map<DexProgramClass, Set<DexMethod>> absentInstanceInitializers) {
assert !group.hasTarget();
assert !group.hasInstanceFieldMap();
@@ -76,7 +132,10 @@
// reference parameters are converted to java.lang.Object), to ensure that merging will result
// in a simple renaming (specifically, we must not need to append null arguments to constructor
// calls due to constructor collisions).
- group.removeIf(this::hasMultipleInstanceInitializersWithSameRelaxedSignature);
+ group.removeIf(
+ clazz ->
+ hasMultipleInstanceInitializersWithSameRelaxedSignature(
+ clazz, absentInstanceInitializers));
if (group.isEmpty()) {
return Collections.emptyList();
@@ -87,14 +146,14 @@
group.selectTarget(appView);
group.selectInstanceFieldMap(appView);
- Map<MergeGroup, Map<DexMethodSignature, ProgramMethod>> newGroups = new LinkedHashMap<>();
+ Map<MergeGroup, Map<DexMethodSignature, InstanceInitializer>> newGroups = new LinkedHashMap<>();
// Caching of instance initializer descriptions, which are used to determine equivalence.
// TODO(b/181846319): Make this cache available to the instance initializer merger so that we
// don't reanalyze instance initializers.
- ProgramMethodMap<Optional<InstanceInitializerDescription>> instanceInitializerDescriptions =
- ProgramMethodMap.create();
- Function<ProgramMethod, Optional<InstanceInitializerDescription>>
+ Map<DexMethod, Optional<InstanceInitializerDescription>> instanceInitializerDescriptions =
+ new IdentityHashMap<>();
+ Function<InstanceInitializer, Optional<InstanceInitializerDescription>>
instanceInitializerDescriptionProvider =
instanceInitializer ->
getOrComputeInstanceInitializerDescription(
@@ -104,11 +163,12 @@
// collisions.
for (DexProgramClass clazz : group) {
MergeGroup newGroup = null;
- Map<DexMethodSignature, ProgramMethod> classInstanceInitializers =
- getInstanceInitializersByRelaxedSignature(clazz);
- for (Entry<MergeGroup, Map<DexMethodSignature, ProgramMethod>> entry : newGroups.entrySet()) {
+ Map<DexMethodSignature, InstanceInitializer> classInstanceInitializers =
+ getInstanceInitializersByRelaxedSignature(clazz, absentInstanceInitializers);
+ for (Entry<MergeGroup, Map<DexMethodSignature, InstanceInitializer>> entry :
+ newGroups.entrySet()) {
MergeGroup candidateGroup = entry.getKey();
- Map<DexMethodSignature, ProgramMethod> groupInstanceInitializers = entry.getValue();
+ Map<DexMethodSignature, InstanceInitializer> groupInstanceInitializers = entry.getValue();
if (canAddClassToGroup(
classInstanceInitializers,
groupInstanceInitializers,
@@ -132,14 +192,16 @@
}
private boolean canAddClassToGroup(
- Map<DexMethodSignature, ProgramMethod> classInstanceInitializers,
- Map<DexMethodSignature, ProgramMethod> groupInstanceInitializers,
- Function<ProgramMethod, Optional<InstanceInitializerDescription>>
+ Map<DexMethodSignature, InstanceInitializer> classInstanceInitializers,
+ Map<DexMethodSignature, InstanceInitializer> groupInstanceInitializers,
+ Function<InstanceInitializer, Optional<InstanceInitializerDescription>>
instanceInitializerDescriptionProvider) {
- for (Entry<DexMethodSignature, ProgramMethod> entry : classInstanceInitializers.entrySet()) {
+ for (Entry<DexMethodSignature, InstanceInitializer> entry :
+ classInstanceInitializers.entrySet()) {
DexMethodSignature relaxedSignature = entry.getKey();
- ProgramMethod classInstanceInitializer = entry.getValue();
- ProgramMethod groupInstanceInitializer = groupInstanceInitializers.get(relaxedSignature);
+ InstanceInitializer classInstanceInitializer = entry.getValue();
+ InstanceInitializer groupInstanceInitializer =
+ groupInstanceInitializers.get(relaxedSignature);
if (groupInstanceInitializer == null) {
continue;
}
@@ -160,31 +222,41 @@
return true;
}
- private boolean hasMultipleInstanceInitializersWithSameRelaxedSignature(DexProgramClass clazz) {
- Iterator<ProgramMethod> instanceInitializers = clazz.programInstanceInitializers().iterator();
- if (!instanceInitializers.hasNext()) {
- // No instance initializers.
+ private boolean hasMultipleInstanceInitializersWithSameRelaxedSignature(
+ DexProgramClass clazz, Map<DexProgramClass, Set<DexMethod>> absentInstanceInitializers) {
+ Set<DexMethod> instanceInitializerReferences =
+ SetUtils.unionIdentityHashSet(
+ SetUtils.newIdentityHashSet(
+ IterableUtils.transform(
+ clazz.programInstanceInitializers(), ProgramMethod::getReference)),
+ absentInstanceInitializers.getOrDefault(clazz, Collections.emptySet()));
+ if (instanceInitializerReferences.size() <= 1) {
return false;
}
- ProgramMethod first = instanceInitializers.next();
- if (!instanceInitializers.hasNext()) {
- // Only a single instance initializer.
- return false;
- }
-
- Set<DexMethod> seen = SetUtils.newIdentityHashSet(getRelaxedSignature(first));
- return Iterators.any(
- instanceInitializers,
- instanceInitializer -> !seen.add(getRelaxedSignature(instanceInitializer)));
+ Set<DexMethod> seen = SetUtils.newIdentityHashSet();
+ return Iterables.any(
+ instanceInitializerReferences,
+ instanceInitializerReference ->
+ !seen.add(getRelaxedSignature(instanceInitializerReference)));
}
- private Map<DexMethodSignature, ProgramMethod> getInstanceInitializersByRelaxedSignature(
- DexProgramClass clazz) {
- Map<DexMethodSignature, ProgramMethod> result = new HashMap<>();
- for (ProgramMethod instanceInitializer : clazz.programInstanceInitializers()) {
- DexMethodSignature relaxedSignature = getRelaxedSignature(instanceInitializer).getSignature();
- ProgramMethod previous = result.put(relaxedSignature, instanceInitializer);
+ private Map<DexMethodSignature, InstanceInitializer> getInstanceInitializersByRelaxedSignature(
+ DexProgramClass clazz, Map<DexProgramClass, Set<DexMethod>> absentInstanceInitializers) {
+ Map<DexMethodSignature, InstanceInitializer> result = new HashMap<>();
+ for (ProgramMethod presentInstanceInitializer : clazz.programInstanceInitializers()) {
+ DexMethodSignature relaxedSignature =
+ getRelaxedSignature(presentInstanceInitializer).getSignature();
+ InstanceInitializer previous =
+ result.put(relaxedSignature, new PresentInstanceInitializer(presentInstanceInitializer));
+ assert previous == null;
+ }
+ for (DexMethod absentInstanceInitializer :
+ absentInstanceInitializers.getOrDefault(clazz, Collections.emptySet())) {
+ DexMethodSignature relaxedSignature =
+ getRelaxedSignature(absentInstanceInitializer).getSignature();
+ InstanceInitializer previous =
+ result.put(relaxedSignature, new AbsentInstanceInitializer(absentInstanceInitializer));
assert previous == null;
}
return result;
@@ -192,10 +264,10 @@
private Optional<InstanceInitializerDescription> getOrComputeInstanceInitializerDescription(
MergeGroup group,
- ProgramMethod instanceInitializer,
- ProgramMethodMap<Optional<InstanceInitializerDescription>> instanceInitializerDescriptions) {
+ InstanceInitializer instanceInitializer,
+ Map<DexMethod, Optional<InstanceInitializerDescription>> instanceInitializerDescriptions) {
return instanceInitializerDescriptions.computeIfAbsent(
- instanceInitializer,
+ instanceInitializer.getReference(),
key -> {
InstanceInitializerDescription instanceInitializerDescription =
InstanceInitializerAnalysis.analyze(
@@ -205,15 +277,20 @@
}
private DexMethod getRelaxedSignature(ProgramMethod instanceInitializer) {
+ return getRelaxedSignature(instanceInitializer.getReference());
+ }
+
+ private DexMethod getRelaxedSignature(DexMethod instanceInitializerReference) {
DexType objectType = appView.dexItemFactory().objectType;
- DexTypeList parameters = instanceInitializer.getParameters();
+ DexTypeList parameters = instanceInitializerReference.getParameters();
DexTypeList relaxedParameters =
parameters.map(parameter -> parameter.isPrimitiveType() ? parameter : objectType);
return parameters != relaxedParameters
? appView
.dexItemFactory()
- .createInstanceInitializer(instanceInitializer.getHolderType(), relaxedParameters)
- : instanceInitializer.getReference();
+ .createInstanceInitializer(
+ instanceInitializerReference.getHolderType(), relaxedParameters)
+ : instanceInitializerReference;
}
private void setInstanceFieldMaps(Iterable<MergeGroup> newGroups, MergeGroup group) {
diff --git a/src/main/java/com/android/tools/r8/utils/MapUtils.java b/src/main/java/com/android/tools/r8/utils/MapUtils.java
index 7c603b2..5757218 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.utils;
import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
import java.util.Collections;
@@ -13,6 +14,7 @@
import java.util.Map.Entry;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Supplier;
@@ -41,9 +43,10 @@
return ignore -> supplier.get();
}
- public static <K, V> IdentityHashMap<K, V> newIdentityHashMap(BiForEachable<K, V> forEachable) {
+ public static <K, V> IdentityHashMap<K, V> newIdentityHashMap(
+ Consumer<IdentityHashMap<K, V>> builder) {
IdentityHashMap<K, V> map = new IdentityHashMap<>();
- forEachable.forEach(map::put);
+ builder.accept(map);
return map;
}
@@ -54,6 +57,13 @@
return map;
}
+ public static <K, V> ImmutableMap<K, V> newImmutableMap(
+ Consumer<ImmutableMap.Builder<K, V>> consumer) {
+ ImmutableMap.Builder<K, V> builder = ImmutableMap.builder();
+ consumer.accept(builder);
+ return builder.build();
+ }
+
public static <T> void removeIdentityMappings(Map<T, T> map) {
map.entrySet().removeIf(entry -> entry.getKey() == entry.getValue());
}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingTest.java
index 70f55b4..8f46be5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingTest.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,15 +45,13 @@
.addOptionsModification(
options -> options.horizontalClassMergerOptions().disableInitialRoundOfClassMerging())
.addHorizontallyMergedClassesInspector(
- inspector ->
- inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+ HorizontallyMergedClassesInspector::assertNoClassesMerged)
.enableInliningAnnotations()
.enableNeverClassInliningAnnotations()
.setMinApi(parameters)
.compile()
.run(parameters.getRuntime(), Main.class)
- // TODO(b/276385221): Should not trigger A.<init>.
- .assertSuccessWithOutputLines("Ouch!", "B");
+ .assertSuccessWithOutputLines("B");
}
static class Main {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java
new file mode 100644
index 0000000..578c482
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java
@@ -0,0 +1,168 @@
+// Copyright (c) 2023, 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.KeepUnusedArguments;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Similar to {@link HorizontalClassMergingAfterConstructorShrinkingTest}, but extended so that
+ * {@code B.<init>(Parent)} needs to be correctly lens rewritten in the horizontal class merger,
+ * since {@link Parent} is subject to repackaging, which runs prior to horizontal class merging.
+ */
+@RunWith(Parameterized.class)
+public class HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withDexRuntimes()
+ .withApiLevelsStartingAtIncluding(AndroidApiLevel.L)
+ .build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ assertTrue(parameters.canHaveNonReboundConstructorInvoke());
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addKeepRules("-repackageclasses")
+ .addOptionsModification(
+ options -> options.horizontalClassMergerOptions().disableInitialRoundOfClassMerging())
+ .addHorizontallyMergedClassesInspector(
+ HorizontallyMergedClassesInspector::assertNoClassesMerged)
+ .enableConstantArgumentAnnotations()
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .enableUnusedArgumentAnnotations()
+ .setMinApi(parameters)
+ .compile()
+ .inspect(
+ inspector -> {
+ // Verify Parent and Parent.<init>(Parent) are present and that Parent has been
+ // repackaged
+ // into the default package.
+ ClassSubject parentClassSubject = inspector.clazz(Parent.class);
+ assertThat(parentClassSubject, isPresent());
+ assertEquals("", parentClassSubject.getDexProgramClass().getType().getPackageName());
+
+ MethodSubject parentInstanceInitializerSubject =
+ parentClassSubject.uniqueInstanceInitializer();
+ assertThat(parentInstanceInitializerSubject, isPresent());
+ assertEquals(
+ parentClassSubject.asTypeSubject(),
+ parentInstanceInitializerSubject.getParameter(0));
+
+ // Verify that A and A.<init>(Parent) are present.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertEquals("", aClassSubject.getDexProgramClass().getType().getPackageName());
+
+ MethodSubject aInstanceInitializerSubject = aClassSubject.uniqueInstanceInitializer();
+ assertThat(aInstanceInitializerSubject, isPresent());
+ assertEquals(
+ parentClassSubject.asTypeSubject(), aInstanceInitializerSubject.getParameter(0));
+
+ // Verify that B's initializer was removed.
+ ClassSubject bClassSubject = inspector.clazz(B.class);
+ assertThat(bClassSubject, isPresent());
+ assertEquals("", bClassSubject.getDexProgramClass().getType().getPackageName());
+ assertEquals(
+ 0, bClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+ })
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("B");
+ }
+
+ public static class Main {
+
+ static {
+ new B(null).setFieldOnB().printFieldOnB();
+ }
+
+ public static void main(String[] args) {
+ if (System.currentTimeMillis() < 0) {
+ new A(null).setFieldOnA().printFieldOnA();
+ }
+ }
+ }
+
+ public static class Parent {
+
+ @KeepConstantArguments
+ @KeepUnusedArguments
+ public Parent(Parent parent) {}
+ }
+
+ @NeverClassInline
+ public static class A extends Parent {
+
+ Object field;
+
+ @KeepConstantArguments
+ public A(Parent parent) {
+ super(parent);
+ System.out.println("Ouch!");
+ }
+
+ @NeverInline
+ public A setFieldOnA() {
+ field = "A";
+ return this;
+ }
+
+ @NeverInline
+ public void printFieldOnA() {
+ System.out.println(field);
+ }
+ }
+
+ @NeverClassInline
+ public static class B extends Parent {
+
+ Object field;
+
+ // Removed by constructor shrinking.
+ @KeepConstantArguments
+ public B(Parent parent) {
+ super(parent);
+ }
+
+ @NeverInline
+ public B setFieldOnB() {
+ field = "B";
+ return this;
+ }
+
+ @NeverInline
+ public void printFieldOnB() {
+ System.out.println(field);
+ }
+ }
+}