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);
+    }
+  }
+}