Context-sensitive missing classes reporting

This CL also uses the new contextual information to avoid reporting missing classes that are only referenced from contexts, which are matched by -dontwarn rules.

Bug: 175755807
Bug: 122881119
Bug: 179249745
Change-Id: Iedb85ee27864d43c93dd6bf2f30cb4eb728aeb03
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index c11016d..a79b303 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -327,7 +327,8 @@
                   .appInfo()
                   .rebuildWithClassHierarchy(
                       MissingClasses.builderForInitialMissingClasses()
-                          .addNewMissingClasses(new SubtypingInfo(appView).getMissingClasses())
+                          .legacyAddNewMissingClasses(
+                              new SubtypingInfo(appView).getMissingClasses())
                           .reportMissingClasses(appView)));
         }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index adbe83c..f806886 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -7,6 +7,8 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
@@ -46,6 +48,10 @@
     this.descriptor = descriptor;
   }
 
+  public ClassReference asClassReference() {
+    return Reference.classFromDescriptor(toDescriptorString());
+  }
+
   @Override
   public DexType self() {
     return this;
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 d028682..716f2dc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -550,7 +550,9 @@
   }
 
   private void recordTypeReference(
-      DexType type, ProgramDefinition context, Consumer<DexType> missingClassConsumer) {
+      DexType type,
+      ProgramDefinition context,
+      BiConsumer<DexType, DexReference> missingClassConsumer) {
     if (type == null) {
       return;
     }
@@ -569,7 +571,9 @@
   }
 
   private void recordMethodReference(
-      DexMethod method, ProgramDefinition context, Consumer<DexType> missingClassConsumer) {
+      DexMethod method,
+      ProgramDefinition context,
+      BiConsumer<DexType, DexReference> missingClassConsumer) {
     recordTypeReference(method.holder, context, missingClassConsumer);
     recordTypeReference(method.proto.returnType, context, missingClassConsumer);
     for (DexType type : method.proto.parameters.values) {
@@ -595,15 +599,19 @@
   }
 
   private DexClass definitionFor(
-      DexType type, ProgramDefinition context, Consumer<DexType> missingClassConsumer) {
+      DexType type,
+      ProgramDefinition context,
+      BiConsumer<DexType, DexReference> missingClassConsumer) {
     return internalDefinitionFor(type, context, missingClassConsumer);
   }
 
   private DexClass internalDefinitionFor(
-      DexType type, ProgramDefinition context, Consumer<DexType> missingClassConsumer) {
+      DexType type,
+      ProgramDefinition context,
+      BiConsumer<DexType, DexReference> missingClassConsumer) {
     DexClass clazz = appInfo().definitionFor(type);
     if (clazz == null) {
-      missingClassConsumer.accept(type);
+      missingClassConsumer.accept(type, context.getReference());
       return null;
     }
     if (clazz.isNotProgramClass()) {
@@ -661,7 +669,7 @@
     }
     DexClass definition = appView.definitionFor(type);
     if (definition == null) {
-      reportMissingClass(type);
+      reportMissingClassWithoutContext(type);
       return;
     }
     if (definition.isProgramClass() || !liveNonProgramTypes.add(definition)) {
@@ -2243,7 +2251,11 @@
     missingClassesBuilder.ignoreNewMissingClass(clazz);
   }
 
-  private void reportMissingClass(DexType clazz) {
+  private void ignoreMissingClass(DexType clazz, DexReference context) {
+    ignoreMissingClass(clazz);
+  }
+
+  private void reportMissingClass(DexType clazz, DexReference context) {
     assert !mode.isFinalTreeShaking()
             || missingClassesBuilder.wasAlreadyMissing(clazz)
             || appView.dexItemFactory().isPossiblyCompilerSynthesizedType(clazz)
@@ -2251,7 +2263,19 @@
             // TODO(b/157107464): See if we can clean this up.
             || (initialPrunedTypes != null && initialPrunedTypes.contains(clazz))
         : "Unexpected missing class `" + clazz.toSourceString() + "`";
-    missingClassesBuilder.addNewMissingClass(clazz);
+    missingClassesBuilder.addNewMissingClass(clazz, context);
+  }
+
+  @Deprecated
+  private void reportMissingClassWithoutContext(DexType clazz) {
+    assert !mode.isFinalTreeShaking()
+            || missingClassesBuilder.wasAlreadyMissing(clazz)
+            || appView.dexItemFactory().isPossiblyCompilerSynthesizedType(clazz)
+            || initialDeadProtoTypes.contains(clazz)
+            // TODO(b/157107464): See if we can clean this up.
+            || (initialPrunedTypes != null && initialPrunedTypes.contains(clazz))
+        : "Unexpected missing class `" + clazz.toSourceString() + "`";
+    missingClassesBuilder.legacyAddNewMissingClass(clazz);
   }
 
   private void reportMissingMethod(DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/shaking/MissingClasses.java b/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
index 1def3a9..7c0395a 100644
--- a/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
+++ b/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
@@ -8,15 +8,20 @@
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter.getRetargetPackageAndClassPrefixDescriptor;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.EMULATE_LIBRARY_CLASS_NAME_SUFFIX;
 
+import com.android.tools.r8.errors.dontwarn.DontWarnConfiguration;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.synthesis.CommittedItems;
 import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableList;
+import com.android.tools.r8.utils.SetUtils;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -54,7 +59,7 @@
   public static class Builder {
 
     private final Set<DexType> alreadyMissingClasses;
-    private final Set<DexType> newMissingClasses = Sets.newIdentityHashSet();
+    private final Map<DexType, Set<DexReference>> newMissingClasses = new IdentityHashMap<>();
 
     // Set of missing types that are not to be reported as missing. This does not hide reports
     // if the same type is in newMissingClasses in which case it is reported regardless.
@@ -68,12 +73,25 @@
       this.alreadyMissingClasses = alreadyMissingClasses;
     }
 
-    public void addNewMissingClass(DexType type) {
-      newMissingClasses.add(type);
+    public void addNewMissingClass(DexType type, DexReference context) {
+      assert context != null;
+      assert context.getContextType() != type;
+      if (!alreadyMissingClasses.contains(type)) {
+        newMissingClasses.computeIfAbsent(type, ignore -> Sets.newIdentityHashSet()).add(context);
+      }
     }
 
-    public Builder addNewMissingClasses(Collection<DexType> types) {
-      newMissingClasses.addAll(types);
+    public void legacyAddNewMissingClass(DexType type) {
+      if (!alreadyMissingClasses.contains(type)) {
+        // The legacy reporting is context insensitive, so therefore we use the missing classes
+        // themselves as contexts.
+        newMissingClasses.computeIfAbsent(type, ignore -> Sets.newIdentityHashSet()).add(type);
+      }
+    }
+
+    @Deprecated
+    public Builder legacyAddNewMissingClasses(Collection<DexType> types) {
+      types.forEach(this::legacyAddNewMissingClass);
       return this;
     }
 
@@ -82,7 +100,7 @@
     }
 
     public boolean contains(DexType type) {
-      return alreadyMissingClasses.contains(type) || newMissingClasses.contains(type);
+      return alreadyMissingClasses.contains(type) || newMissingClasses.containsKey(type);
     }
 
     Builder removeAlreadyMissingClasses(Iterable<DexType> types) {
@@ -99,17 +117,12 @@
 
     public MissingClasses reportMissingClasses(AppView<?> appView) {
       InternalOptions options = appView.options();
-      Set<DexType> newMissingClassesWithoutDontWarn =
-          appView.getDontWarnConfiguration().getNonMatches(newMissingClasses);
-      newMissingClassesWithoutDontWarn.removeAll(alreadyMissingClasses);
-      newMissingClassesWithoutDontWarn.removeAll(
-          getAllowedMissingClasses(appView.dexItemFactory()));
-      newMissingClassesWithoutDontWarn.removeIf(
-          isCompilerSynthesizedAllowedMissingClasses(appView));
-      if (!newMissingClassesWithoutDontWarn.isEmpty()) {
+      Map<DexType, Set<DexReference>> missingClassesToBeReported =
+          getMissingClassesToBeReported(appView);
+      if (!missingClassesToBeReported.isEmpty()) {
         MissingClassesDiagnostic diagnostic =
             new MissingClassesDiagnostic.Builder()
-                .addMissingClasses(newMissingClassesWithoutDontWarn)
+                .addMissingClasses(missingClassesToBeReported)
                 .setFatal(!options.ignoreMissingClasses)
                 .build();
         if (options.ignoreMissingClasses) {
@@ -121,8 +134,44 @@
       return build();
     }
 
-    private static Collection<DexType> getAllowedMissingClasses(DexItemFactory dexItemFactory) {
-      return ImmutableList.<DexType>builder()
+    private Map<DexType, Set<DexReference>> getMissingClassesToBeReported(AppView<?> appView) {
+      Predicate<DexType> allowedMissingClassesPredicate =
+          getIsAllowedMissingClassesPredicate(appView);
+      Map<DexType, Set<DexReference>> missingClassesToBeReported =
+          new IdentityHashMap<>(newMissingClasses.size());
+      newMissingClasses.forEach(
+          (missingClass, contexts) -> {
+            // Don't report "allowed" missing classes (e.g., classes matched by -dontwarn).
+            if (allowedMissingClassesPredicate.test(missingClass)) {
+              return;
+            }
+
+            // Remove all contexts that are matched by a -dontwarn rule (a missing class should not
+            // be reported if it os only referenced from contexts that are matched by a -dontwarn).
+            contexts.removeIf(
+                context -> appView.getDontWarnConfiguration().matches(context.getContextType()));
+
+            // If there are any contexts not matched by a -dontwarn rule, then report.
+            if (!contexts.isEmpty()) {
+              missingClassesToBeReported.put(missingClass, contexts);
+            }
+          });
+      return missingClassesToBeReported;
+    }
+
+    private static Predicate<DexType> getIsAllowedMissingClassesPredicate(AppView<?> appView) {
+      Set<DexType> allowedMissingClasses = getAllowedMissingClasses(appView.dexItemFactory());
+      Predicate<DexType> compilerSynthesizedAllowingMissingClassPredicate =
+          getIsCompilerSynthesizedAllowedMissingClassesPredicate(appView);
+      DontWarnConfiguration dontWarnConfiguration = appView.getDontWarnConfiguration();
+      return type ->
+          allowedMissingClasses.contains(type)
+              || compilerSynthesizedAllowingMissingClassPredicate.test(type)
+              || dontWarnConfiguration.matches(type);
+    }
+
+    private static Set<DexType> getAllowedMissingClasses(DexItemFactory dexItemFactory) {
+      return ImmutableSet.<DexType>builder()
           .add(
               dexItemFactory.annotationDefault,
               dexItemFactory.annotationMethodParameters,
@@ -133,14 +182,14 @@
               dexItemFactory.serializedLambdaType,
               // TODO(b/176133674) StringConcatFactory is backported, but the class is reported as
               //  missing because the enqueuer runs prior to backporting and thus sees the
-              // non-desugared
-              //  code.
+              //  non-desugared code.
               dexItemFactory.stringConcatFactoryType)
           .addAll(dexItemFactory.getConversionTypes())
           .build();
     }
 
-    private Predicate<DexType> isCompilerSynthesizedAllowedMissingClasses(AppView<?> appView) {
+    private static Predicate<DexType> getIsCompilerSynthesizedAllowedMissingClassesPredicate(
+        AppView<?> appView) {
       DexItemFactory dexItemFactory = appView.dexItemFactory();
       InternalOptions options = appView.options();
       DexString emulatedLibraryClassNameSuffix =
@@ -161,7 +210,7 @@
 
     /** Intentionally private, use {@link Builder#reportMissingClasses(AppView)}. */
     private MissingClasses build() {
-      // Extend the newMissingClasses set with all other missing classes.
+      // Return the new set of missing classes.
       //
       // We also add newIgnoredMissingClasses to newMissingClasses to be able to assert that we have
       // a closed world after the first round of tree shaking: we should never lookup a class that
@@ -171,9 +220,9 @@
       // Note: At this point, all missing classes in newMissingClasses have already been reported.
       // Thus adding newIgnoredMissingClasses to newMissingClasses will not lead to reports for the
       // classes in newIgnoredMissingClasses.
-      newMissingClasses.addAll(alreadyMissingClasses);
-      newMissingClasses.addAll(newIgnoredMissingClasses);
-      return new MissingClasses(newMissingClasses);
+      return new MissingClasses(
+          SetUtils.newIdentityHashSet(
+              alreadyMissingClasses, newMissingClasses.keySet(), newIgnoredMissingClasses));
     }
 
     public boolean wasAlreadyMissing(DexType type) {
diff --git a/src/main/java/com/android/tools/r8/shaking/MissingClassesDiagnostic.java b/src/main/java/com/android/tools/r8/shaking/MissingClassesDiagnostic.java
index 5965cc5..2e59a42 100644
--- a/src/main/java/com/android/tools/r8/shaking/MissingClassesDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/shaking/MissingClassesDiagnostic.java
@@ -4,34 +4,131 @@
 
 package com.android.tools.r8.shaking;
 
+import static com.android.tools.r8.utils.PredicateUtils.not;
+
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
-import com.google.common.collect.ImmutableSortedSet;
-import java.util.Collection;
+import com.android.tools.r8.utils.FieldReferenceUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.Set;
-import java.util.SortedSet;
+import java.util.SortedMap;
+import java.util.function.Function;
 
 @Keep
 public class MissingClassesDiagnostic implements Diagnostic {
 
-  private final boolean fatal;
-  private final SortedSet<ClassReference> missingClasses;
+  private static class MissingClassAccessContexts {
 
-  private MissingClassesDiagnostic(boolean fatal, SortedSet<ClassReference> missingClasses) {
+    private ImmutableSet<ClassReference> classContexts;
+    private ImmutableSet<FieldReference> fieldContexts;
+    private ImmutableSet<MethodReference> methodContexts;
+
+    private MissingClassAccessContexts(
+        ImmutableSet<ClassReference> classContexts,
+        ImmutableSet<FieldReference> fieldContexts,
+        ImmutableSet<MethodReference> methodContexts) {
+      this.classContexts = classContexts;
+      this.fieldContexts = fieldContexts;
+      this.methodContexts = methodContexts;
+    }
+
+    static Builder builder() {
+      return new Builder();
+    }
+
+    String getReferencedFromMessageSuffix(ClassReference missingClass) {
+      if (!methodContexts.isEmpty()) {
+        return " (referenced from: "
+            + MethodReferenceUtils.toSourceString(methodContexts.iterator().next())
+            + ")";
+      }
+      if (!fieldContexts.isEmpty()) {
+        return " (referenced from: "
+            + FieldReferenceUtils.toSourceString(fieldContexts.iterator().next())
+            + ")";
+      }
+      // TODO(b/175543745): The legacy reporting is context insensitive, and therefore uses the
+      //  missing classes as their own context. Once legacy reporting is removed, this should be
+      //  simplified to taking the first context.
+      Optional<ClassReference> classContext =
+          classContexts.stream().filter(not(missingClass::equals)).findFirst();
+      if (classContext.isPresent()) {
+        return " (referenced from: " + classContext.toString() + ")";
+      }
+      return "";
+    }
+
+    static class Builder {
+
+      private final Set<DexReference> contexts = Sets.newIdentityHashSet();
+
+      Builder addAll(Set<DexReference> contexts) {
+        this.contexts.addAll(contexts);
+        return this;
+      }
+
+      // TODO(b/179249745): Sort on demand in getReferencedFromMessageSuffix() instead.
+      MissingClassAccessContexts build() {
+        // Sort the contexts for deterministic reporting.
+        List<DexType> classContexts = new ArrayList<>();
+        List<DexField> fieldContexts = new ArrayList<>();
+        List<DexMethod> methodContexts = new ArrayList<>();
+        contexts.forEach(
+            context -> context.apply(classContexts::add, fieldContexts::add, methodContexts::add));
+        Collections.sort(classContexts);
+        Collections.sort(fieldContexts);
+        Collections.sort(methodContexts);
+
+        // Build immutable sets (which preserve insertion order) from the sorted lists, mapping each
+        // DexType, DexField, and DexMethod to ClassReference, FieldReference, and MethodReference,
+        // respectively.
+        return new MissingClassAccessContexts(
+            toImmutableSet(classContexts, DexType::asClassReference),
+            toImmutableSet(fieldContexts, DexField::asFieldReference),
+            toImmutableSet(methodContexts, DexMethod::asMethodReference));
+      }
+
+      private <S, T> ImmutableSet<T> toImmutableSet(List<S> list, Function<S, T> fn) {
+        ImmutableSet.Builder<T> builder = ImmutableSet.builder();
+        list.forEach(element -> builder.add(fn.apply(element)));
+        return builder.build();
+      }
+    }
+  }
+
+  private final boolean fatal;
+  private final SortedMap<ClassReference, MissingClassAccessContexts> missingClasses;
+
+  private MissingClassesDiagnostic(
+      boolean fatal, SortedMap<ClassReference, MissingClassAccessContexts> missingClasses) {
     assert !missingClasses.isEmpty();
     this.fatal = fatal;
     this.missingClasses = missingClasses;
   }
 
   public Set<ClassReference> getMissingClasses() {
-    return missingClasses;
+    return missingClasses.keySet();
   }
 
   /** A missing class(es) failure can generally not be attributed to a single origin. */
@@ -46,7 +143,6 @@
     return Position.UNKNOWN;
   }
 
-  // TODO(b/175755807): Extend diagnostic message with contextual information.
   @Override
   public String getDiagnosticMessage() {
     return fatal ? getFatalDiagnosticMessage() : getNonFatalDiagnosticMessage();
@@ -54,43 +150,70 @@
 
   private String getFatalDiagnosticMessage() {
     if (missingClasses.size() == 1) {
-      return "Compilation can't be completed because the class "
-          + missingClasses.iterator().next().getTypeName()
-          + " is missing.";
+      StringBuilder builder =
+          new StringBuilder(
+              "Compilation can't be completed because the following class is missing: ");
+      writeMissingClass(builder, missingClasses.entrySet().iterator().next());
+      return builder.append(".").toString();
     }
+
     StringBuilder builder =
         new StringBuilder("Compilation can't be completed because the following ")
             .append(missingClasses.size())
             .append(" classes are missing:");
-    for (ClassReference missingClass : missingClasses) {
-      builder.append(System.lineSeparator()).append("- ").append(missingClass.getTypeName());
-    }
+    missingClasses.forEach(
+        (missingClass, contexts) ->
+            writeMissingClass(
+                builder.append(System.lineSeparator()).append("- "), missingClass, contexts));
     return builder.toString();
   }
 
   private String getNonFatalDiagnosticMessage() {
     StringBuilder builder = new StringBuilder();
-    Iterator<ClassReference> missingClassesIterator = missingClasses.iterator();
-    while (missingClassesIterator.hasNext()) {
-      ClassReference missingClass = missingClassesIterator.next();
-      builder.append("Missing class ").append(missingClass.getTypeName());
-      if (missingClassesIterator.hasNext()) {
-        builder.append(System.lineSeparator());
-      }
-    }
+    Iterator<Entry<ClassReference, MissingClassAccessContexts>> missingClassesIterator =
+        missingClasses.entrySet().iterator();
+
+    // The diagnostic is always non-empty.
+    assert missingClassesIterator.hasNext();
+
+    // Write first line.
+    writeMissingClass(builder.append("Missing class "), missingClassesIterator.next());
+
+    // Write remaining lines with line separator before.
+    missingClassesIterator.forEachRemaining(
+        missingClassInfo ->
+            writeMissingClass(
+                builder.append(System.lineSeparator()).append("Missing class "), missingClassInfo));
+
     return builder.toString();
   }
 
+  private static void writeMissingClass(
+      StringBuilder builder, Entry<ClassReference, MissingClassAccessContexts> missingClassInfo) {
+    writeMissingClass(builder, missingClassInfo.getKey(), missingClassInfo.getValue());
+  }
+
+  private static void writeMissingClass(
+      StringBuilder builder, ClassReference missingClass, MissingClassAccessContexts contexts) {
+    builder
+        .append(missingClass.getTypeName())
+        .append(contexts.getReferencedFromMessageSuffix(missingClass));
+  }
+
   public static class Builder {
 
     private boolean fatal;
-    private ImmutableSortedSet.Builder<ClassReference> missingClassesBuilder =
-        ImmutableSortedSet.orderedBy(Comparator.comparing(ClassReference::getDescriptor));
+    private ImmutableSortedMap.Builder<ClassReference, MissingClassAccessContexts>
+        missingClassesBuilder =
+            ImmutableSortedMap.orderedBy(Comparator.comparing(ClassReference::getDescriptor));
 
-    public MissingClassesDiagnostic.Builder addMissingClasses(Collection<DexType> missingClasses) {
-      for (DexType missingClass : missingClasses) {
-        missingClassesBuilder.add(Reference.classFromDescriptor(missingClass.toDescriptorString()));
-      }
+    public MissingClassesDiagnostic.Builder addMissingClasses(
+        Map<DexType, Set<DexReference>> missingClasses) {
+      missingClasses.forEach(
+          (missingClass, contexts) ->
+              missingClassesBuilder.put(
+                  Reference.classFromDescriptor(missingClass.toDescriptorString()),
+                  MissingClassAccessContexts.builder().addAll(contexts).build()));
       return this;
     }
 
diff --git a/src/main/java/com/android/tools/r8/utils/FieldReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/FieldReferenceUtils.java
new file mode 100644
index 0000000..cb26884
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/FieldReferenceUtils.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2020, 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.utils;
+
+import com.android.tools.r8.references.FieldReference;
+
+public class FieldReferenceUtils {
+
+  public static String toSourceString(FieldReference fieldReference) {
+    return fieldReference.getFieldType().getTypeName()
+        + " "
+        + fieldReference.getHolderClass().getTypeName()
+        + "."
+        + fieldReference.getFieldName();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
index 7f48f2a..b62e36e 100644
--- a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
@@ -14,11 +14,20 @@
     return toSourceString(methodReference, false, false);
   }
 
+  public static String toSourceString(MethodReference methodReference) {
+    return toSourceString(methodReference, true, true);
+  }
+
   public static String toSourceString(
       MethodReference methodReference, boolean includeHolder, boolean includeReturnType) {
     StringBuilder builder = new StringBuilder();
     if (includeReturnType) {
-      builder.append(methodReference.getReturnType().getTypeName()).append(" ");
+      builder
+          .append(
+              methodReference.getReturnType() != null
+                  ? methodReference.getReturnType().getTypeName()
+                  : "void")
+          .append(" ");
     }
     if (includeHolder) {
       builder.append(methodReference.getHolderClass().getTypeName()).append(".");
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index c67225c..022f5d9 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -37,6 +37,14 @@
     return result;
   }
 
+  public static <T> Set<T> newIdentityHashSet(Iterable<T> c1, Iterable<T> c2, Iterable<T> c3) {
+    Set<T> result = Sets.newIdentityHashSet();
+    c1.forEach(result::add);
+    c2.forEach(result::add);
+    c3.forEach(result::add);
+    return result;
+  }
+
   public static <T> Set<T> newIdentityHashSet(int capacity) {
     return Collections.newSetFromMap(new IdentityHashMap<>(capacity));
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java
index 66e2ec7..a9e05a3 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java
@@ -63,9 +63,7 @@
     testMissingNestHostError(true);
     testIncompleteNestError(true);
   }
-  // TODO R8:
-  // appView.options().reportMissingNestHost(clazz);
-  // clazz.clearNestHost();
+
   @Test
   public void testWarningR8() throws Exception {
     testIncompleteNestWarning(false, parameters.isDexRuntime());
@@ -98,7 +96,6 @@
           .setMinApi(parameters.getApiLevel())
           .addProgramFiles(matchingClasses)
           .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
-          .addDontWarn("java.lang.invoke.StringConcatFactory")
           .addOptionsModification(
               options -> {
                 options.ignoreMissingClasses = ignoreMissingClasses;
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromNewInstanceTest.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromNewInstanceTest.java
index 8f43ab8..975e84e 100644
--- a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromNewInstanceTest.java
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromNewInstanceTest.java
@@ -9,9 +9,13 @@
 
 import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.shaking.MissingClassesDiagnostic;
+import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
 
 public class MissingClassReferencedFromNewInstanceTest extends MissingClassesTestBase {
@@ -24,31 +28,60 @@
   @Test
   public void test() throws Exception {
     AssertUtils.assertFailsCompilationIf(
-        // TODO(b/175542052): Should not fail compilation with -dontwarn Main.
-        !getDontWarnConfiguration().isDontWarnMissingClass(),
+        getDontWarnConfiguration().isNone(),
         () ->
             compileWithExpectedDiagnostics(
                 Main.class, MissingClass.class, this::inspectDiagnostics));
   }
 
   private void inspectDiagnostics(TestDiagnosticMessages diagnostics) {
-    // TODO(b/175542052): Should also not have any diagnostics with -dontwarn Main.
-    if (getDontWarnConfiguration().isDontWarnMissingClass()) {
+    // There should be no diagnostics with -dontwarn.
+    if (getDontWarnConfiguration().isDontWarn()) {
       diagnostics.assertNoMessages();
       return;
     }
 
-    diagnostics
-        .assertOnlyErrors()
-        .assertErrorsCount(1)
-        .assertAllErrorsMatch(diagnosticType(MissingClassesDiagnostic.class));
+    // There should be a single warning if we are compiling with -ignorewarnings. Otherwise, there
+    // should be a single error.
+    MissingClassesDiagnostic diagnostic;
+    if (getDontWarnConfiguration().isIgnoreWarnings()) {
+      diagnostics
+          .assertOnlyWarnings()
+          .assertWarningsCount(1)
+          .assertAllWarningsMatch(diagnosticType(MissingClassesDiagnostic.class));
+      diagnostic = (MissingClassesDiagnostic) diagnostics.getWarnings().get(0);
+    } else {
+      diagnostics
+          .assertOnlyErrors()
+          .assertErrorsCount(1)
+          .assertAllErrorsMatch(diagnosticType(MissingClassesDiagnostic.class));
+      diagnostic = (MissingClassesDiagnostic) diagnostics.getErrors().get(0);
+    }
 
-    MissingClassesDiagnostic diagnostic = (MissingClassesDiagnostic) diagnostics.getErrors().get(0);
+    // Inspect the diagnostic.
     assertEquals(
-        !getDontWarnConfiguration().isDontWarnMissingClass(),
-        diagnostic.getMissingClasses().stream()
-            .map(TypeReference::getTypeName)
-            .anyMatch(MissingClass.class.getTypeName()::equals));
+        ImmutableSet.of(Reference.classFromClass(MissingClass.class)),
+        diagnostic.getMissingClasses());
+    assertEquals(getExpectedDiagnosticMessage(), diagnostic.getDiagnosticMessage());
+  }
+
+  private String getExpectedDiagnosticMessage() {
+    ClassReference missingClass = Reference.classFromClass(MissingClass.class);
+    MethodReference context;
+    try {
+      context = Reference.methodFromMethod(Main.class.getDeclaredMethod("main", String[].class));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+    String referencedFromSuffix =
+        " (referenced from: " + MethodReferenceUtils.toSourceString(context) + ")";
+    if (getDontWarnConfiguration().isIgnoreWarnings()) {
+      return "Missing class " + missingClass.getTypeName() + referencedFromSuffix;
+    }
+    return "Compilation can't be completed because the following class is missing: "
+        + missingClass.getTypeName()
+        + referencedFromSuffix
+        + ".";
   }
 
   static class Main {
@@ -57,6 +90,4 @@
       new MissingClass();
     }
   }
-
-  static class MissingClass {}
 }
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 7c72273..769ac98 100644
--- a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
@@ -21,8 +21,13 @@
   enum DontWarnConfiguration {
     DONT_WARN_MAIN_CLASS,
     DONT_WARN_MISSING_CLASS,
+    IGNORE_WARNINGS,
     NONE;
 
+    public boolean isDontWarn() {
+      return isDontWarnMainClass() || isDontWarnMissingClass();
+    }
+
     public boolean isDontWarnMainClass() {
       return this == DONT_WARN_MAIN_CLASS;
     }
@@ -30,8 +35,18 @@
     public boolean isDontWarnMissingClass() {
       return this == DONT_WARN_MISSING_CLASS;
     }
+
+    public boolean isIgnoreWarnings() {
+      return this == IGNORE_WARNINGS;
+    }
+
+    public boolean isNone() {
+      return this == NONE;
+    }
   }
 
+  static class MissingClass {}
+
   private final DontWarnConfiguration dontWarnConfiguration;
   private final TestParameters parameters;
 
@@ -53,12 +68,16 @@
     return testForR8(parameters.getBackend())
         .addProgramClasses(mainClass)
         .addKeepMainRule(mainClass)
+        .allowDiagnosticWarningMessages(dontWarnConfiguration.isIgnoreWarnings())
         .applyIf(
             dontWarnConfiguration.isDontWarnMainClass(),
             testBuilder -> testBuilder.addDontWarn(mainClass))
         .applyIf(
             dontWarnConfiguration.isDontWarnMissingClass(),
             testBuilder -> testBuilder.addDontWarn(missingClass))
+        .applyIf(
+            dontWarnConfiguration.isIgnoreWarnings(),
+            testBuilder -> testBuilder.addKeepRules("-ignorewarnings"))
         .addOptionsModification(TestingOptions::enableExperimentalMissingClassesReporting)
         .setMinApi(parameters.getApiLevel())
         .compileWithExpectedDiagnostics(diagnosticsConsumer);
diff --git a/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java b/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java
index 927decb..d01d917 100644
--- a/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java
+++ b/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java
@@ -46,10 +46,6 @@
                 .build()));
   }
 
-  private boolean isDesugaring() {
-    return parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.N);
-  }
-
   @Test
   public void testReference() throws Exception {
     testForRuntime(parameters)
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index 5ad87de..7ba46d2 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.ToolHelper.EXAMPLES_BUILD_DIR;
 import static com.android.tools.r8.ToolHelper.EXAMPLES_DIR;
+import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.Diagnostic;
@@ -26,7 +27,6 @@
 import java.util.Collection;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -92,12 +92,10 @@
         new DiagnosticsHandler() {
           @Override
           public void error(Diagnostic error) {
-            String expectedErrorMessage =
-                "Compilation can't be completed because the class java.lang.Object is missing.";
-            if (error.getDiagnosticMessage().equals(expectedErrorMessage)) {
-              return;
-            }
-            throw new RuntimeException("Unexpected compilation error");
+            assertEquals(
+                "Compilation can't be completed because the following class is missing: "
+                    + "java.lang.Object.",
+                error.getDiagnosticMessage());
           }
         };
     R8Command.Builder builder = R8Command.builder(handler)
@@ -146,7 +144,7 @@
                     "shaking1",
                     "print-mapping-" + backend.name().toLowerCase() + ".ref")),
             StandardCharsets.UTF_8);
-    Assert.assertEquals(sorted(refMapping), sorted(actualMapping));
+    assertEquals(sorted(refMapping), sorted(actualMapping));
   }
 
   private static String sorted(String str) {
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java
index e9634a5..174e734 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java
@@ -9,6 +9,8 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.utils.ThrowingAction;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
 import java.util.function.Consumer;
 
 public class AssertUtils {
@@ -54,7 +56,7 @@
         action.execute();
         fail("Expected action to fail with an exception, but succeeded");
       } catch (Throwable e) {
-        assertEquals(clazz, e.getClass());
+        assertEquals(printStackTraceToString(e), clazz, e.getClass());
         if (consumer != null) {
           consumer.accept(e);
         }
@@ -63,4 +65,12 @@
       action.execute();
     }
   }
+
+  private static String printStackTraceToString(Throwable e) {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (PrintStream ps = new PrintStream(baos)) {
+      e.printStackTrace(ps);
+    }
+    return baos.toString();
+  }
 }