Check PermittedSubclassesAttribute for missing classes

Bug: b/227160052
Change-Id: Ic2b6c76d589b81a91cfd678c193a439ba0406680
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 1ea86b5..4bbc625 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -80,6 +80,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl;
+import com.android.tools.r8.graph.PermittedSubclassAttribute;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramDerivedContext;
 import com.android.tools.r8.graph.ProgramField;
@@ -2098,6 +2099,23 @@
       }
     }
 
+    // Mark types in permitted-subclasses attributes referenced.
+    List<PermittedSubclassAttribute> permittedSubclassAttributes =
+        clazz.getPermittedSubclassAttributes();
+    if (!permittedSubclassAttributes.isEmpty()) {
+      BiConsumer<DexType, ProgramDerivedContext> missingClassConsumer =
+          options.reportMissingClassesInPermittedSubclassesAttributes
+              ? this::reportMissingClass
+              : this::ignoreMissingClass;
+      for (PermittedSubclassAttribute permittedSubclassAttribute : permittedSubclassAttributes) {
+        recordTypeReference(
+            permittedSubclassAttribute.getPermittedSubclass(),
+            clazz,
+            this::recordNonProgramClass,
+            missingClassConsumer);
+      }
+    }
+
     KeepReason reason = KeepReason.reachableFromLiveType(clazz.type);
 
     for (DexType iface : clazz.getInterfaces()) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index c284670..83b37c3 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -876,6 +876,7 @@
   public boolean ignoreMissingClasses = false;
   public boolean reportMissingClassesInEnclosingMethodAttribute = false;
   public boolean reportMissingClassesInInnerClassAttributes = false;
+  public boolean reportMissingClassesInPermittedSubclassesAttributes = false;
   public boolean disableGenericSignatureValidation = false;
   public boolean disableInnerClassSeparatorValidationWhenRepackaging = false;
 
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java
index 03d9160..87baa3f 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java
@@ -37,7 +37,6 @@
 
   @Parameter(1)
   public boolean keepPermittedSubclassesAttribute;
-
   static final Matcher<String> EXPECTED = containsString("cannot inherit from sealed class");
   static final String EXPECTED_WITHOUT_PERMITTED_SUBCLASSES_ATTRIBUTE =
       StringUtils.lines("Success!");
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromPermittedSubclassesAttributeTest.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromPermittedSubclassesAttributeTest.java
new file mode 100644
index 0000000..af6954b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromPermittedSubclassesAttributeTest.java
@@ -0,0 +1,152 @@
+// 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.missingclasses;
+
+import static com.android.tools.r8.utils.codeinspector.AssertUtils.assertFailsCompilationIf;
+import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.diagnostic.DefinitionContext;
+import com.android.tools.r8.diagnostic.internal.DefinitionClassContextImpl;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+public class MissingClassReferencedFromPermittedSubclassesAttributeTest
+    extends MissingClassesTestBase {
+
+  private static final DefinitionContext referencedFrom =
+      DefinitionClassContextImpl.builder()
+          .setClassContext(Reference.classFromClass(Super.class))
+          .setOrigin(getOrigin(Super.class))
+          .build();
+
+  @Parameters(name = "{1}, report: {0}")
+  public static List<Object[]> refinedData() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  private final boolean reportMissingClassesInPermittedSubclassesAttributes;
+
+  public MissingClassReferencedFromPermittedSubclassesAttributeTest(
+      boolean reportMissingClassesInPermittedSubclassesAttributes, TestParameters parameters) {
+    super(parameters);
+    this.reportMissingClassesInPermittedSubclassesAttributes =
+        reportMissingClassesInPermittedSubclassesAttributes;
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // Missing classes stays in the PermittedSubclasses attribute.
+    assertEquals(
+        parameters.isCfRuntime()
+            ? ImmutableList.of(
+                inspector.clazz(Sub.class).asTypeSubject(),
+                inspector.getTypeSubject(MissingSub.class.getTypeName()))
+            : ImmutableList.of(),
+        inspector.clazz(Super.class).getFinalPermittedSubclassAttributes());
+  }
+
+  @Test()
+  public void testNoRules() throws Exception {
+    assertFailsCompilationIf(
+        reportMissingClassesInPermittedSubclassesAttributes,
+        () ->
+            compileWithExpectedDiagnostics(
+                Main.class,
+                reportMissingClassesInPermittedSubclassesAttributes
+                    ? diagnostics -> inspectDiagnosticsWithNoRules(diagnostics, referencedFrom)
+                    : TestDiagnosticMessages::assertNoMessages,
+                this::configure));
+  }
+
+  @Test
+  public void testDontWarnSuperClass() throws Exception {
+    compileWithExpectedDiagnostics(
+            Main.class,
+            TestDiagnosticMessages::assertNoMessages,
+            addDontWarn(Super.class).andThen(this::configure))
+        .inspect(this::inspect);
+  }
+
+  @Test
+  public void testDontWarnMissingClass() throws Exception {
+    compileWithExpectedDiagnostics(
+            Main.class,
+            TestDiagnosticMessages::assertNoMessages,
+            addDontWarn(MissingSub.class).andThen(this::configure))
+        .inspect(this::inspect);
+  }
+
+  @Test
+  public void testIgnoreWarnings() throws Exception {
+    compileWithExpectedDiagnostics(
+            Main.class,
+            reportMissingClassesInPermittedSubclassesAttributes
+                ? diagnostics -> inspectDiagnosticsWithIgnoreWarnings(diagnostics, referencedFrom)
+                : TestDiagnosticMessages::assertNoMessages,
+            addIgnoreWarnings(reportMissingClassesInPermittedSubclassesAttributes)
+                .andThen(this::configure))
+        .inspect(this::inspect);
+  }
+
+  void configure(R8FullTestBuilder builder) {
+    try {
+      builder
+          .addKeepAttributePermittedSubclasses()
+          .addProgramClasses(Sub.class)
+          .addProgramClassFileData(getTransformedClasses())
+          .addKeepClassRulesWithAllowObfuscation(Super.class, Sub.class)
+          .addOptionsModification(
+              options -> {
+                // We do not report missing classes from permitted subclasses attributes by default.
+                assertFalse(options.reportMissingClassesInPermittedSubclassesAttributes);
+                options.reportMissingClassesInPermittedSubclassesAttributes =
+                    reportMissingClassesInPermittedSubclassesAttributes;
+              })
+          .applyIf(
+              !reportMissingClassesInPermittedSubclassesAttributes,
+              // The -dontwarn Main and -dontwarn MissingClass tests will have unused -dontwarn
+              // rules.
+              R8TestBuilder::allowUnusedDontWarnPatterns);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  ClassReference getMissingClassReference() {
+    return Reference.classFromClass(MissingSub.class);
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub.class, MissingSub.class)
+        .transform();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new Sub();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub, MissingSub */ {}
+
+  static class Sub extends Super {}
+
+  static class MissingSub extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
index 5cc5fe1..91c8722 100644
--- a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder.DiagnosticsConsumer;
 import com.android.tools.r8.TestDiagnosticMessages;
@@ -75,27 +76,27 @@
     compileWithExpectedDiagnostics(mainClass, diagnosticsConsumer, null);
   }
 
-  public void compileWithExpectedDiagnostics(
+  public R8TestCompileResult compileWithExpectedDiagnostics(
       Class<?> mainClass,
       DiagnosticsConsumer diagnosticsConsumer,
       ThrowableConsumer<R8FullTestBuilder> configuration)
       throws CompilationFailedException {
-    internalCompileWithExpectedDiagnostics(
+    return internalCompileWithExpectedDiagnostics(
         diagnosticsConsumer,
         builder ->
             builder.addProgramClasses(mainClass).addKeepMainRule(mainClass).apply(configuration));
   }
 
-  public void compileWithExpectedDiagnostics(
+  public R8TestCompileResult compileWithExpectedDiagnostics(
       ThrowableConsumer<R8FullTestBuilder> configuration, DiagnosticsConsumer diagnosticsConsumer)
       throws CompilationFailedException {
-    internalCompileWithExpectedDiagnostics(diagnosticsConsumer, configuration);
+    return internalCompileWithExpectedDiagnostics(diagnosticsConsumer, configuration);
   }
 
-  private void internalCompileWithExpectedDiagnostics(
+  private R8TestCompileResult internalCompileWithExpectedDiagnostics(
       DiagnosticsConsumer diagnosticsConsumer, ThrowableConsumer<R8FullTestBuilder> configuration)
       throws CompilationFailedException {
-    testForR8(parameters.getBackend())
+    return testForR8(parameters.getBackend())
         .apply(configuration)
         .setMinApi(parameters)
         .compileWithExpectedDiagnostics(diagnosticsConsumer);