Update the message for the MissingDefinitionsDiagnostic

Bug: 169127026
Bug: 169546956
Change-Id: Id251613a961edc4f8a21d77b1aac571d4f2dbfc1
diff --git a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
index db31dac..8fd9edb 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
@@ -10,6 +10,7 @@
 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 com.android.tools.r8.tracereferences.Tracer.TracedReferenceBase;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -40,18 +41,27 @@
     return Position.UNKNOWN;
   }
 
+  private <T extends TracedReferenceBase<?, ?>> void appendSorted(
+      StringBuilder builder, Set<T> missing) {
+    missing.stream()
+        .map(element -> element.getReference())
+        .map(Object::toString)
+        .sorted()
+        .forEach(item -> builder.append("  ").append(item).append(System.lineSeparator()));
+  }
+
   @Override
   public String getDiagnosticMessage() {
     StringBuilder builder = new StringBuilder("Tracereferences found ");
     List<String> components = new ArrayList<>();
     if (missingClasses.size() > 0) {
-      components.add("" + missingClasses.size() + " classes");
+      components.add("" + missingClasses.size() + " classe(s)");
     }
     if (missingFields.size() > 0) {
-      components.add("" + missingClasses.size() + " fields");
+      components.add("" + missingFields.size() + " field(s)");
     }
     if (missingMethods.size() > 0) {
-      components.add("" + missingClasses.size() + " methods");
+      components.add("" + missingMethods.size() + " method(s)");
     }
     assert components.size() > 0;
     for (int i = 0; i < components.size(); i++) {
@@ -63,27 +73,12 @@
     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()));
+    builder.append("Classe(s) without definition:" + System.lineSeparator());
+    appendSorted(builder, missingClasses);
+    builder.append("Field(s) without definition:" + System.lineSeparator());
+    appendSorted(builder, missingFields);
+    builder.append("Method(s) without definition:" + System.lineSeparator());
+    appendSorted(builder, missingMethods);
     return builder.toString();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
new file mode 100644
index 0000000..b21d409
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
@@ -0,0 +1,220 @@
+// 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 static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TraceReferencesDiagnosticTest extends TestBase {
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public TraceReferencesDiagnosticTest(TestParameters parameters) {}
+
+  @Test
+  public void traceReferencesDiagnosticClassesFieldsAndMethods() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar =
+        ZipBuilder.builder(dir.resolve("target.jar"))
+            .addBytes(
+                DescriptorUtils.getPathFromJavaType(Target.class),
+                transformer(Target.class)
+                    .removeFields(
+                        (access, name, descriptor, signature, value) ->
+                            name.equals("missingField1"))
+                    .removeFields(
+                        (access, name, descriptor, signature, value) ->
+                            name.equals("missingField2"))
+                    .removeMethods(
+                        (access, name, descriptor, signature, exceptions) ->
+                            name.equals("missingMethod"))
+                    .transform())
+            .build();
+    Path sourceJar =
+        ZipBuilder.builder(dir.resolve("source.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Source.class))
+            .build();
+
+    String prefix = "  Lcom/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest$";
+    try {
+      DiagnosticsChecker.checkErrorsContains(
+          ImmutableList.of(
+              "Tracereferences found 3 classe(s), 2 field(s) and 4 method(s) without definition",
+              StringUtils.lines(
+                  "Classe(s) without definition:",
+                  prefix + "Target1;",
+                  prefix + "Target2;",
+                  prefix + "Target3;"),
+              StringUtils.lines(
+                  "Field(s) without definition:",
+                  prefix + "Target;missingField1:I",
+                  prefix + "Target;missingField2:I"),
+              StringUtils.lines(
+                  "Method(s) without definition:",
+                  prefix + "Target1;<init>()V",
+                  prefix + "Target2;<init>()V",
+                  prefix + "Target3;<init>()V",
+                  prefix + "Target;missingMethod()V")),
+          handler ->
+              TraceReferences.run(
+                  TraceReferencesCommand.builder(handler)
+                      .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+                      .addSourceFiles(sourceJar)
+                      .addTargetFiles(targetJar)
+                      .setConsumer(TraceReferencesConsumer.emptyConsumer())
+                      .build()));
+      fail("Unexpected success");
+    } catch (CompilationFailedException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void traceReferencesDiagnosticFieldsAndMethods() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar =
+        ZipBuilder.builder(dir.resolve("target.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Target1.class),
+                ToolHelper.getClassFileForTestClass(Target2.class),
+                ToolHelper.getClassFileForTestClass(Target3.class))
+            .addBytes(
+                DescriptorUtils.getPathFromJavaType(Target.class),
+                transformer(Target.class)
+                    .removeFields(
+                        (access, name, descriptor, signature, value) ->
+                            name.equals("missingField1"))
+                    .removeFields(
+                        (access, name, descriptor, signature, value) ->
+                            name.equals("missingField2"))
+                    .removeMethods(
+                        (access, name, descriptor, signature, exceptions) ->
+                            name.equals("missingMethod"))
+                    .transform())
+            .build();
+    Path sourceJar =
+        ZipBuilder.builder(dir.resolve("source.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Source.class))
+            .build();
+
+    String prefix = "  Lcom/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest$";
+    try {
+      DiagnosticsChecker.checkErrorsContains(
+          ImmutableList.of(
+              "Tracereferences found 2 field(s) and 1 method(s) without definition",
+              StringUtils.lines(
+                  "Field(s) without definition:",
+                  prefix + "Target;missingField1:I",
+                  prefix + "Target;missingField2:I"),
+              StringUtils.lines(
+                  "Method(s) without definition:", prefix + "Target;missingMethod()V")),
+          handler ->
+              TraceReferences.run(
+                  TraceReferencesCommand.builder(handler)
+                      .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+                      .addSourceFiles(sourceJar)
+                      .addTargetFiles(targetJar)
+                      .setConsumer(TraceReferencesConsumer.emptyConsumer())
+                      .build()));
+      fail("Unexpected success");
+    } catch (CompilationFailedException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void traceReferencesDiagnosticMethods() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar =
+        ZipBuilder.builder(dir.resolve("target.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Target1.class),
+                ToolHelper.getClassFileForTestClass(Target2.class),
+                ToolHelper.getClassFileForTestClass(Target3.class))
+            .addBytes(
+                DescriptorUtils.getPathFromJavaType(Target.class),
+                transformer(Target.class)
+                    .removeMethods(
+                        (access, name, descriptor, signature, exceptions) ->
+                            name.equals("missingMethod"))
+                    .transform())
+            .build();
+    Path sourceJar =
+        ZipBuilder.builder(dir.resolve("source.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Source.class))
+            .build();
+
+    String prefix = "  Lcom/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest$";
+    try {
+      DiagnosticsChecker.checkErrorsContains(
+          ImmutableList.of(
+              "Tracereferences found 1 method(s) without definition",
+              StringUtils.lines(
+                  "Method(s) without definition:", prefix + "Target;missingMethod()V")),
+          handler ->
+              TraceReferences.run(
+                  TraceReferencesCommand.builder(handler)
+                      .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+                      .addSourceFiles(sourceJar)
+                      .addTargetFiles(targetJar)
+                      .setConsumer(TraceReferencesConsumer.emptyConsumer())
+                      .build()));
+      fail("Unexpected success");
+    } catch (CompilationFailedException e) {
+      // Expected.
+    }
+  }
+
+  static class Target1 {}
+
+  static class Target2 {}
+
+  static class Target3 {}
+
+  static class Target {
+    public static int missingField1;
+    public static int missingField2;
+
+    public static void missingMethod() {}
+  }
+
+  static class Source {
+    public static void source() {
+      new Target1();
+      new Target2();
+      new Target3();
+
+      Target.missingField1 = 1;
+      Target.missingField2 = 2;
+      Target.missingMethod();
+    }
+  }
+}