Use a specific type for missing definitions in tracereferences

Bug: 169127026
Bug: 169546956
Change-Id: I1e5e374c9a4830ab59c2e5146f68b42449b09a07
diff --git a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
new file mode 100644
index 0000000..db31dac
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
@@ -0,0 +1,89 @@
+// 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.tracereferences;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.tracereferences.Tracer.TracedClassImpl;
+import com.android.tools.r8.tracereferences.Tracer.TracedFieldImpl;
+import com.android.tools.r8.tracereferences.Tracer.TracedMethodImpl;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@Keep
+public class MissingDefinitionsDiagnostic implements Diagnostic {
+
+  private final Set<TracedClassImpl> missingClasses;
+  private final Set<TracedFieldImpl> missingFields;
+  private final Set<TracedMethodImpl> missingMethods;
+
+  MissingDefinitionsDiagnostic(
+      Set<TracedClassImpl> missingClasses,
+      Set<TracedFieldImpl> missingFields,
+      Set<TracedMethodImpl> missingMethods) {
+    this.missingClasses = missingClasses;
+    this.missingFields = missingFields;
+    this.missingMethods = missingMethods;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return Origin.unknown();
+  }
+
+  @Override
+  public Position getPosition() {
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    StringBuilder builder = new StringBuilder("Tracereferences found ");
+    List<String> components = new ArrayList<>();
+    if (missingClasses.size() > 0) {
+      components.add("" + missingClasses.size() + " classes");
+    }
+    if (missingFields.size() > 0) {
+      components.add("" + missingClasses.size() + " fields");
+    }
+    if (missingMethods.size() > 0) {
+      components.add("" + missingClasses.size() + " methods");
+    }
+    assert components.size() > 0;
+    for (int i = 0; i < components.size(); i++) {
+      if (i != 0) {
+        builder.append(i < components.size() - 1 ? ", " : " and ");
+      }
+      builder.append(components.get(i));
+    }
+    builder.append(" without definition");
+    builder.append(System.lineSeparator());
+    builder.append(System.lineSeparator());
+    builder.append("Classes without definition:");
+    missingClasses.forEach(
+        clazz ->
+            builder
+                .append("  ")
+                .append(clazz.getReference().toString())
+                .append(System.lineSeparator()));
+    builder.append("Fields without definition");
+    missingFields.forEach(
+        field ->
+            builder
+                .append("  ")
+                .append(field.getReference().toString())
+                .append(System.lineSeparator()));
+    builder.append("Methods without definition");
+    missingMethods.forEach(
+        method ->
+            builder
+                .append("  ")
+                .append(method.getReference().toString())
+                .append(System.lineSeparator()));
+    return builder.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index 1149b20..110fb8b 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -41,7 +41,6 @@
 import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedReference;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.Timing;
 import java.io.IOException;
 import java.util.HashSet;
@@ -251,6 +250,7 @@
       clazz.forEachField(useCollector::registerField);
     }
     consumer.finished();
+    useCollector.reportMissingDefinitions();
   }
 
   class UseCollector extends UseRegistry {
@@ -259,7 +259,9 @@
     private final TraceReferencesConsumer consumer;
     private DexProgramClass context;
     private final DiagnosticsHandler diagnostics;
-    private Set<TracedReference<?, ?>> missingDefinitionReported = new HashSet<>();
+    private final Set<TracedClassImpl> missingClasses = new HashSet<>();
+    private final Set<TracedFieldImpl> missingFields = new HashSet<>();
+    private final Set<TracedMethodImpl> missingMethods = new HashSet<>();
 
     UseCollector(
         DexItemFactory factory, TraceReferencesConsumer consumer, DiagnosticsHandler diagnostics) {
@@ -283,7 +285,7 @@
       }
       DexClass clazz = appInfo.definitionFor(type);
       TracedClassImpl tracedClass = new TracedClassImpl(type, clazz);
-      checkDiagnostics(tracedClass);
+      checkMissingDefinition(tracedClass);
       if (isTargetType(type) || tracedClass.isMissingDefinition()) {
         consumer.acceptType(tracedClass);
         if (!tracedClass.isMissingDefinition()
@@ -301,7 +303,7 @@
       }
       addType(field.holder);
       TracedFieldImpl tracedField = new TracedFieldImpl(field, baseField);
-      checkDiagnostics(tracedField);
+      checkMissingDefinition(tracedField);
       if (isTargetType(field.holder) || tracedField.isMissingDefinition()) {
         consumer.acceptField(tracedField);
         if (!tracedField.isMissingDefinition()
@@ -322,7 +324,7 @@
       TracedMethodImpl tracedMethod = new TracedMethodImpl(method, definition);
       if (isTargetType(method.holder) || tracedMethod.isMissingDefinition()) {
         consumer.acceptMethod(tracedMethod);
-        checkDiagnostics(tracedMethod);
+        checkMissingDefinition(tracedMethod);
         if (!tracedMethod.isMissingDefinition()
             && definition.accessFlags.isVisibilityDependingOnPackage()) {
           consumer.acceptPackage(Reference.packageFromString(definition.holder().getPackageName()));
@@ -330,11 +332,29 @@
       }
     }
 
-    private void checkDiagnostics(TracedReferenceBase<?, ?> tracedReference) {
-      if (tracedReference.isMissingDefinition() && missingDefinitionReported.add(tracedReference)) {
+    private void checkMissingDefinition(TracedClassImpl tracedClass) {
+      collectMissing(tracedClass, missingClasses);
+    }
+
+    private void checkMissingDefinition(TracedFieldImpl tracedField) {
+      collectMissing(tracedField, missingFields);
+    }
+
+    private void checkMissingDefinition(TracedMethodImpl tracedMethod) {
+      collectMissing(tracedMethod, missingMethods);
+    }
+
+    private <T extends TracedReferenceBase<?, ?>> void collectMissing(
+        T tracedReference, Set<T> missingCollection) {
+      if (tracedReference.isMissingDefinition()) {
+        missingCollection.add(tracedReference);
+      }
+    }
+
+    private void reportMissingDefinitions() {
+      if (missingClasses.size() > 0 || missingFields.size() > 0 || missingMethods.size() > 0) {
         diagnostics.error(
-            new StringDiagnostic(
-                "Missing definition of " + tracedReference.getKindName() + " " + tracedReference));
+            new MissingDefinitionsDiagnostic(missingClasses, missingFields, missingMethods));
       }
     }
 
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index 730d952..09e32da 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -60,6 +60,10 @@
         diagnostics.stream().anyMatch(d -> d.getDiagnosticMessage().contains(snippet)));
   }
 
+  public void checkErrorsContains(String snippet) {
+    checkContains(snippet, errors);
+  }
+
   public void checkWarningsContains(String snippet) {
     checkContains(snippet, warnings);
   }
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
index a312c2f..430af64 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.tracereferences.TraceReferencesFormattingConsumer.OutputFormat;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -23,6 +24,8 @@
 import com.google.common.collect.ImmutableList;
 import java.io.BufferedOutputStream;
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
@@ -135,14 +138,13 @@
   }
 
   private String formatName(OutputFormat format) {
-    if (format == TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE) {
+    if (format == OutputFormat.PRINTUSAGE) {
       return "printuses";
     }
-    if (format == TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES) {
+    if (format == OutputFormat.KEEP_RULES) {
       return "keep";
     }
-    assertSame(
-        format, TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION);
+    assertSame(format, OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION);
     return "keepallowobfuscation";
   }
 
@@ -233,12 +235,12 @@
     runAndCheckOutput(
         ImmutableList.of(Target.class),
         ImmutableList.of(Source.class),
-        TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE,
+        OutputFormat.PRINTUSAGE,
         StringUtils.lines(
             ImmutableList.of(
                 "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
                 "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
-                    + " target(int)",
+                    + " method(int)",
                 "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
                     + " field")));
   }
@@ -248,12 +250,12 @@
     runAndCheckOutput(
         ImmutableList.of(Target.class),
         ImmutableList.of(Source.class),
-        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES,
+        OutputFormat.KEEP_RULES,
         StringUtils.lines(
             ImmutableList.of(
                 "-keep class com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
                     + " {",
-                "  public static void target(int);",
+                "  public static void method(int);",
                 "  int field;",
                 "}",
                 "-keeppackagenames com.android.tools.r8.tracereferences")));
@@ -264,31 +266,43 @@
     runAndCheckOutput(
         ImmutableList.of(Target.class),
         ImmutableList.of(Source.class),
-        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
+        OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
         StringUtils.lines(
             ImmutableList.of(
                 "-keep,allowobfuscation class"
                     + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target {",
-                "  public static void target(int);",
+                "  public static void method(int);",
                 "  int field;",
                 "}",
                 "-keeppackagenames com.android.tools.r8.tracereferences")));
   }
 
+  private void checkTargetMissing(DiagnosticsChecker diagnosticsChecker) {
+    Field field;
+    Method method;
+    try {
+      field = Target.class.getField("field");
+      method = Target.class.getMethod("method", int.class);
+    } catch (ReflectiveOperationException e) {
+      throw new RuntimeException(e);
+    }
+    assertEquals(1, diagnosticsChecker.errors.size());
+    assertEquals(0, diagnosticsChecker.warnings.size());
+    assertEquals(0, diagnosticsChecker.infos.size());
+    diagnosticsChecker.checkErrorsContains(Reference.classFromClass(Target.class).toString());
+    diagnosticsChecker.checkErrorsContains(Reference.fieldFromField(field).toString());
+    diagnosticsChecker.checkErrorsContains(Reference.methodFromMethod(method).toString());
+  }
+
   @Test
   public void testNoReferences_printUses() throws Throwable {
-    String targetTypeName = Target.class.getTypeName();
     try {
       runAndCheckOutput(
           ImmutableList.of(OtherTarget.class),
           ImmutableList.of(Source.class),
-          TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE,
+          OutputFormat.PRINTUSAGE,
           StringUtils.lines(),
-          diagnosticsChecker -> {
-            assertEquals(3, diagnosticsChecker.errors.size());
-            assertEquals(0, diagnosticsChecker.warnings.size());
-            assertEquals(0, diagnosticsChecker.infos.size());
-          });
+          this::checkTargetMissing);
       fail("Expected compilation to fail");
     } catch (CompilationFailedException e) {
       // Expected.
@@ -297,18 +311,15 @@
 
   @Test
   public void testMissingReference_keepRules() throws Throwable {
-    String targetTypeName = Target.class.getTypeName();
+    Field field = Target.class.getField("field");
+    Method method = Target.class.getMethod("method", int.class);
     try {
       runAndCheckOutput(
           ImmutableList.of(OtherTarget.class),
           ImmutableList.of(Source.class),
-          TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES,
+          OutputFormat.KEEP_RULES,
           StringUtils.lines(),
-          diagnosticsChecker -> {
-            assertEquals(3, diagnosticsChecker.errors.size());
-            assertEquals(0, diagnosticsChecker.warnings.size());
-            assertEquals(0, diagnosticsChecker.infos.size());
-          });
+          this::checkTargetMissing);
       fail("Expected compilation to fail");
     } catch (CompilationFailedException e) {
       // Expected.
@@ -316,18 +327,14 @@
   }
 
   @Test
-  public void testNoReferences_keepRulesAllowObfuscation() throws Throwable {
+  public void testMissingReference_keepRulesAllowObfuscation() throws Throwable {
     try {
       runAndCheckOutput(
           ImmutableList.of(OtherTarget.class),
           ImmutableList.of(Source.class),
-          TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
+          OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
           StringUtils.lines(),
-          diagnosticsChecker -> {
-            assertEquals(3, diagnosticsChecker.errors.size());
-            assertEquals(0, diagnosticsChecker.warnings.size());
-            assertEquals(0, diagnosticsChecker.infos.size());
-          });
+          this::checkTargetMissing);
       fail("Expected compilation to fail");
     } catch (CompilationFailedException e) {
       // Expected.
@@ -344,6 +351,22 @@
     }
   }
 
+  private void checkTargetPartlyMissing(DiagnosticsChecker diagnosticsChecker) {
+    Field field;
+    Method method;
+    try {
+      field = Target.class.getField("field");
+      method = Target.class.getMethod("method", int.class);
+    } catch (ReflectiveOperationException e) {
+      throw new RuntimeException(e);
+    }
+    assertEquals(1, diagnosticsChecker.errors.size());
+    assertEquals(0, diagnosticsChecker.warnings.size());
+    assertEquals(0, diagnosticsChecker.infos.size());
+    diagnosticsChecker.checkErrorsContains(Reference.fieldFromField(field).toString());
+    diagnosticsChecker.checkErrorsContains(Reference.methodFromMethod(method).toString());
+  }
+
   @Test
   public void testMissingDefinition_printUses() throws Throwable {
     Path dir = temp.newFolder().toPath();
@@ -358,15 +381,11 @@
       runAndCheckOutput(
           targetJar,
           sourceJar,
-          TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE,
+          OutputFormat.PRINTUSAGE,
           StringUtils.lines(
               ImmutableList.of(
                   "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target")),
-          diagnosticsChecker -> {
-            assertEquals(2, diagnosticsChecker.errors.size());
-            assertEquals(0, diagnosticsChecker.warnings.size());
-            assertEquals(0, diagnosticsChecker.infos.size());
-          });
+          this::checkTargetPartlyMissing);
       fail("Expected compilation to fail");
     } catch (CompilationFailedException e) {
       // Expected.
@@ -387,7 +406,7 @@
       runAndCheckOutput(
           targetJar,
           sourceJar,
-          TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES,
+          OutputFormat.KEEP_RULES,
           StringUtils.lines(
               ImmutableList.of(
                   "-keep class"
@@ -395,11 +414,7 @@
                       + " {",
                   "}",
                   "-keeppackagenames com.android.tools.r8.tracereferences")),
-          diagnosticsChecker -> {
-            assertEquals(2, diagnosticsChecker.errors.size());
-            assertEquals(0, diagnosticsChecker.warnings.size());
-            assertEquals(0, diagnosticsChecker.infos.size());
-          });
+          this::checkTargetPartlyMissing);
       fail("Expected compilation to fail");
     } catch (CompilationFailedException e) {
       // Expected.
@@ -420,18 +435,14 @@
       runAndCheckOutput(
           targetJar,
           sourceJar,
-          TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
+          OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
           StringUtils.lines(
               ImmutableList.of(
                   "-keep,allowobfuscation class"
                       + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target {",
                   "}",
                   "-keeppackagenames com.android.tools.r8.tracereferences")),
-          diagnosticsChecker -> {
-            assertEquals(2, diagnosticsChecker.errors.size());
-            assertEquals(0, diagnosticsChecker.warnings.size());
-            assertEquals(0, diagnosticsChecker.infos.size());
-          });
+          this::checkTargetPartlyMissing);
       fail("Expected compilation to fail");
     } catch (CompilationFailedException e) {
       // Expected.
@@ -440,7 +451,7 @@
 
   private byte[] getClassWithTargetRemoved() throws IOException {
     return transformer(Target.class)
-        .removeMethods((access, name, descriptor, signature, exceptions) -> name.equals("target"))
+        .removeMethods((access, name, descriptor, signature, exceptions) -> name.equals("method"))
         .removeFields((access, name, descriptor, signature, value) -> name.equals("field"))
         .transform();
   }
@@ -448,16 +459,16 @@
   static class Target {
     public static int field;
 
-    public static void target(int i) {}
+    public static void method(int i) {}
   }
 
   static class OtherTarget {
-    public static void target() {}
+    public static void method() {}
   }
 
   static class Source {
     public static void source() {
-      Target.target(Target.field);
+      Target.method(Target.field);
     }
   }
 }