Keep synthetic classes mentioned as argument types to synthetic constructors

For private constructors javac can generate synthetic classes and
additional package private constructors to allow calling private
constructors between inner and outer classes.

These synthetic classes are never instantiated - the argument passed
is always null. However when calling getDeclaredConstructors these
synthetic classes will be loaded, and if they are shaken out the
call to getDeclaredConstructors will fail.

Bug: 69825683
Bug: 71630128
Change-Id: I89bca8a6b11f246df1a4f091354e9f5d03a586d0
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 92dc9cd..1c7e92d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -158,6 +158,14 @@
   }
 
 
+  /**
+   * Returns true if this method is synthetic.
+   */
+  public boolean isSyntheticMethod() {
+    return accessFlags.isSynthetic();
+  }
+
+
   public boolean isInliningCandidate(DexEncodedMethod container, Reason inliningReason,
       AppInfoWithSubtyping appInfo) {
     if (isClassInitializer()) {
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 7ed70bd..aebb0a4 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -702,6 +702,22 @@
     if (!liveMethods.contains(encodedMethod)) {
       markTypeAsLive(encodedMethod.method.holder);
       markMethodAsTargeted(encodedMethod, reason);
+      // For granting inner/outer classes access to their private constructors, javac generates
+      // additional synthetic constructors. These constructors take a synthetic class
+      // as argument. As it is not possible to express a keep rule for these synthetic classes
+      // always keep synthetic arguments to synthetic constructors. See b/69825683.
+      if (encodedMethod.isInstanceInitializer() && encodedMethod.isSyntheticMethod()) {
+        for (DexType type : encodedMethod.method.proto.parameters.values) {
+          type = type.isArrayType() ? type.toBaseType(appInfo.dexItemFactory) : type;
+          if (type.isPrimitiveType()) {
+            continue;
+          }
+          DexClass clazz = appInfo.definitionFor(type);
+          if (clazz != null && clazz.accessFlags.isSynthetic()) {
+            markTypeAsLive(type);
+          }
+        }
+      }
       if (Log.ENABLED) {
         Log.verbose(getClass(), "Method `%s` has become live due to direct invoke",
             encodedMethod.method);
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
new file mode 100644
index 0000000..9026740
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2018, 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.regress.b69825683;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+
+public class Regress69825683Test extends TestBase {
+
+  @Test
+  public void outerConstructsInner() throws Exception {
+    Class mainClass = com.android.tools.r8.regress.b69825683.outerconstructsinner.Outer.class;
+    R8Command.Builder builder = R8Command.builder();
+    builder.addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()));
+    builder.addProguardConfiguration(ImmutableList.of(
+        "-keep class " + mainClass.getCanonicalName() + " {",
+        "  public static void main(java.lang.String[]);",
+        "}",
+        "-dontobfuscate"),
+        Origin.unknown());
+    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    AndroidApp app = ToolHelper.runR8(builder.build());
+    DexInspector inspector = new DexInspector(app);
+    List<FoundClassSubject> classes = inspector.allClasses();
+
+    // Check that the synthetic class is still present.
+    assertEquals(3, classes.size());
+    assertEquals(1,
+        classes.stream()
+            .map(FoundClassSubject::getOriginalName)
+            .filter(name  -> name.endsWith("$1"))
+            .count());
+
+    // Run code to check that the constructor with synthetic class as argument is present.
+    Class innerClass =
+        com.android.tools.r8.regress.b69825683.outerconstructsinner.Outer.Inner.class;
+    String innerName = innerClass.getCanonicalName();
+    int index = innerName.lastIndexOf('.');
+    innerName = innerName.substring(0, index) + "$" + innerName.substring(index + 1);
+    assertTrue(runOnArt(app, mainClass).startsWith(innerName));
+  }
+
+  @Test
+  public void innerConstructsOuter() throws Exception {
+    Class mainClass = com.android.tools.r8.regress.b69825683.innerconstructsouter.Outer.class;
+    R8Command.Builder builder = R8Command.builder();
+    builder.addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()));
+    builder.addProguardConfiguration(ImmutableList.of(
+        "-keep class " + mainClass.getCanonicalName() + " {",
+        "  public static void main(java.lang.String[]);",
+        "}",
+        "-dontobfuscate"),
+        Origin.unknown());
+    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    AndroidApp app = ToolHelper.runR8(builder.build());
+    DexInspector inspector = new DexInspector(app);
+    List<FoundClassSubject> classes = inspector.allClasses();
+
+    // Check that the synthetic class is still present.
+    assertEquals(3, classes.size());
+    assertEquals(1,
+        classes.stream()
+            .map(FoundClassSubject::getOriginalName)
+            .filter(name  -> name.endsWith("$1"))
+            .count());
+
+    // Run code to check that the constructor with synthetic class as argument is present.
+    assertTrue(runOnArt(app, mainClass).startsWith(mainClass.getCanonicalName()));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/innerconstructsouter/Outer.java b/src/test/java/com/android/tools/r8/regress/b69825683/innerconstructsouter/Outer.java
new file mode 100644
index 0000000..8241c18
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/innerconstructsouter/Outer.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.regress.b69825683.innerconstructsouter;
+
+public class Outer {
+
+  private Outer() {
+  }
+
+  public static class Inner {
+    public Outer build() {
+      return new Outer();
+    }
+  }
+
+  public static void main(String args[]) {
+    Inner builder = new Inner();
+    builder.build();
+    for (java.lang.reflect.Constructor m : Outer.class.getDeclaredConstructors()) {
+      System.out.println(m);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/outerconstructsinner/Outer.java b/src/test/java/com/android/tools/r8/regress/b69825683/outerconstructsinner/Outer.java
new file mode 100644
index 0000000..79bccc5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/outerconstructsinner/Outer.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.regress.b69825683.outerconstructsinner;
+
+public class Outer {
+
+  public Outer() {
+    new Inner();
+  }
+
+  public class Inner {
+
+    private Inner() {
+    }
+  }
+
+  public static void main(String args[]) {
+    new Outer();
+    for (java.lang.reflect.Constructor m : Outer.Inner.class.getDeclaredConstructors()) {
+      System.out.println(m);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index bbd6fb5..b8ca32e 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -189,18 +189,19 @@
   }
 
   public void forAllClasses(Consumer<FoundClassSubject> inspection) {
-    forAll(application.classes(), clazz -> {
-      ClassNamingForNameMapper naming = null;
-      if (mapping != null) {
-        String obfuscated = originalToObfuscatedMapping.get(clazz.type.toSourceString());
-        if (obfuscated != null) {
-          naming = mapping.getClassNaming(obfuscated);
-        }
-      }
-      return new FoundClassSubject(clazz, naming);
+    forAll(application.classes(), cls -> {
+      ClassSubject found = clazz(cls.type.toSourceString());
+      assert found.isPresent();
+      return (FoundClassSubject) found;
     }, inspection);
   }
 
+  public List<FoundClassSubject> allClasses() {
+    ImmutableList.Builder<FoundClassSubject> builder = ImmutableList.builder();
+    forAllClasses(builder::add);
+    return builder.build();
+  }
+
   public MethodSubject method(Method method) {
     ClassSubject clazz = clazz(method.getDeclaringClass());
     if (!clazz.isPresent()) {
@@ -310,6 +311,8 @@
 
     public abstract AnnotationSubject annotation(String name);
 
+    public abstract String getOriginalName();
+
     public abstract String getOriginalDescriptor();
 
     public abstract String getFinalDescriptor();
@@ -364,6 +367,11 @@
     }
 
     @Override
+    public String getOriginalName() {
+      return null;
+    }
+
+    @Override
     public String getOriginalDescriptor() {
       return null;
     }
@@ -518,6 +526,15 @@
     }
 
     @Override
+    public String getOriginalName() {
+      if (naming != null) {
+        return naming.originalName;
+      } else {
+        return DescriptorUtils.descriptorToJavaType(getFinalDescriptor());
+      }
+    }
+
+    @Override
     public String getOriginalDescriptor() {
       if (naming != null) {
         return DescriptorUtils.javaTypeToDescriptor(naming.originalName);