Add a structured consumer for tracereferences

Bug: 169127026
Change-Id: Ie43ac267c91d648ec5535f7b983165201c08df41
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Formatter.java b/src/main/java/com/android/tools/r8/tracereferences/Formatter.java
new file mode 100644
index 0000000..b545e65
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/Formatter.java
@@ -0,0 +1,176 @@
+// 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.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.PackageReference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedClass;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedField;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedMethod;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+abstract class Formatter {
+
+  private final StringBuilder output;
+
+  Formatter() {
+    output = new StringBuilder();
+  }
+
+  String get() {
+    return output.toString();
+  }
+
+  protected void append(String string) {
+    output.append(string);
+  }
+
+  protected void appendLine(String string) {
+    output.append(StringUtils.lines(string));
+  }
+
+  protected void appendLine() {
+    appendLine("");
+  }
+
+  protected void printArguments(MethodReference method) {
+    StringUtils.append(
+        output,
+        ListUtils.map(method.getFormalTypes(), TypeReference::getTypeName),
+        ",",
+        BraceType.PARENS);
+  }
+
+  protected abstract void printConstructorName(MethodReference method);
+
+  private void printError(String message) {
+    append("# Error: " + message);
+  }
+
+  protected abstract void printField(TracedField field);
+
+  protected abstract void printMethod(TracedMethod method);
+
+  private void printFieldError(FieldReference field) {
+    appendLine(
+        field.getFieldType().getTypeName()
+            + " "
+            + field.getHolderClass().getTypeName()
+            + "."
+            + field.getFieldName());
+  }
+
+  private void printMethodError(MethodReference method) {
+    printReturn(method);
+    append(" ");
+    append(method.getHolderClass().getTypeName());
+    append(".");
+    append(method.getMethodName());
+    printArguments(method);
+    appendLine();
+  }
+
+  protected abstract void printPackageNames(List<String> packageNames);
+
+  protected void printReturn(MethodReference method) {
+    append(method.getReturnType() != null ? method.getReturnType().getTypeName() : "void");
+  }
+
+  protected void printNameAndReturn(MethodReference method) {
+    if (method.getMethodName().equals("<init>")) {
+      printConstructorName(method);
+    } else {
+      printReturn(method);
+      append(" ");
+      append(method.getMethodName());
+    }
+  }
+
+  protected abstract void printTypeHeader(TracedClass clazz);
+
+  protected abstract void printTypeFooter();
+
+  void format(TraceReferencesResult result) {
+    int errors =
+        print(
+            result.types,
+            result.keepPackageNames,
+            result.fields,
+            result.methods,
+            result.missingDefinition);
+    assert errors == result.missingDefinition.size();
+  }
+
+  private int print(
+      Set<TracedClass> types,
+      Set<PackageReference> keepPackageNames,
+      Map<ClassReference, Set<TracedField>> fields,
+      Map<ClassReference, Set<TracedMethod>> methods,
+      Set<Object> missingDefinition) {
+    int errors = 0;
+    List<TracedClass> sortedTypes = new ArrayList<>(types);
+    sortedTypes.sort(Comparator.comparing(tracedClass -> tracedClass.getReference().getTypeName()));
+    for (TracedClass type : sortedTypes) {
+      if (missingDefinition.contains(type)) {
+        printError("Could not find definition for type " + type.getReference().getTypeName());
+        errors++;
+        continue;
+      }
+      printTypeHeader(type);
+      Set<TracedMethod> methodsForClass = methods.get(type.getReference());
+      if (methodsForClass != null) {
+        List<TracedMethod> sortedMethods = new ArrayList<>(methods.get(type.getReference()).size());
+        for (TracedMethod method : methods.get(type.getReference())) {
+          if (method.isMissingDefinition()) {
+            printError("Could not find definition for method ");
+            printMethodError(method.getReference());
+            errors++;
+            continue;
+          }
+          assert method.getAccessFlags() != null;
+          sortedMethods.add(method);
+        }
+        sortedMethods.sort(
+            Comparator.comparing(tracedMethod -> tracedMethod.getReference().toString()));
+        for (TracedMethod method : sortedMethods) {
+          printMethod(method);
+        }
+      }
+      Set<TracedField> fieldsForClass = fields.get(type.getReference());
+      if (fieldsForClass != null) {
+        List<TracedField> sortedFields = new ArrayList<>(fieldsForClass);
+        sortedFields.sort(
+            Comparator.comparing(tracedField -> tracedField.getReference().toString()));
+        for (TracedField field : sortedFields) {
+          if (field.isMissingDefinition()) {
+            printError("Could not find definition for field ");
+            printFieldError(field.getReference());
+            errors++;
+            continue;
+          }
+          printField(field);
+        }
+      }
+      printTypeFooter();
+    }
+    List<String> packageNamesToKeep =
+        keepPackageNames.stream()
+            .map(PackageReference::getPackageName)
+            .sorted()
+            .collect(Collectors.toList());
+    printPackageNames(packageNamesToKeep);
+    return errors;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java b/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java
index f259b01..85352f2 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java
@@ -3,70 +3,66 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.tracereferences;
 
-import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.StringConsumer;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedClass;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedField;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedMethod;
 import com.android.tools.r8.utils.StringUtils;
 import java.util.List;
 
-class KeepRuleFormatter extends ResultFormatter {
+class KeepRuleFormatter extends Formatter {
   final boolean allowObfuscation;
 
-  KeepRuleFormatter(
-      StringConsumer output, DiagnosticsHandler diagnosticsHandler, boolean allowObfuscation) {
-    super(output, diagnosticsHandler);
+  KeepRuleFormatter(boolean allowObfuscation) {
     this.allowObfuscation = allowObfuscation;
   }
 
   @Override
-  protected void printTypeHeader(DexClass dexClass) {
+  protected void printTypeHeader(TracedClass tracedClass) {
     append(allowObfuscation ? "-keep,allowobfuscation" : "-keep");
-    if (dexClass.isInterface()) {
-      append(" interface " + dexClass.type.toSourceString() + " {" + System.lineSeparator());
-    } else if (dexClass.accessFlags.isEnum()) {
-      append(" enum " + dexClass.type.toSourceString() + " {" + System.lineSeparator());
+    if (tracedClass.getAccessFlags().isInterface()) {
+      appendLine(" interface " + tracedClass.getReference().getTypeName() + " {");
+    } else if (tracedClass.getAccessFlags().isEnum()) {
+      appendLine(" enum " + tracedClass.getReference().getTypeName() + " {");
     } else {
-      append(" class " + dexClass.type.toSourceString() + " {" + System.lineSeparator());
+      appendLine(" class " + tracedClass.getReference().getTypeName() + " {");
     }
   }
 
   @Override
-  protected void printConstructorName(DexEncodedMethod encodedMethod) {
+  protected void printConstructorName(MethodReference method) {
     append("<init>");
   }
 
   @Override
-  protected void printField(DexClass dexClass, DexField field) {
+  protected void printField(TracedField field) {
     append(
         "  "
-            + field.type.toSourceString()
+            + field.getReference().getFieldType().getTypeName()
             + " "
-            + field.name.toString()
+            + field.getReference().getFieldName()
             + ";"
             + System.lineSeparator());
   }
 
   @Override
-  protected void printMethod(DexEncodedMethod encodedMethod, String typeName) {
-    // Static initializers do not require keep rules - it is kept by keeping the class.
-    if (encodedMethod.accessFlags.isConstructor() && encodedMethod.accessFlags.isStatic()) {
+  protected void printMethod(TracedMethod tracedMethod) {
+    if (tracedMethod.getReference().getMethodName().equals("<clinit>")) {
       return;
     }
     append("  ");
-    if (encodedMethod.isPublicMethod()) {
+    if (tracedMethod.getAccessFlags().isPublic()) {
       append("public ");
-    } else if (encodedMethod.isPrivateMethod()) {
+    } else if (tracedMethod.getAccessFlags().isPrivate()) {
       append("private ");
-    } else if (encodedMethod.isProtectedMethod()) {
+    } else if (tracedMethod.getAccessFlags().isProtected()) {
       append("protected ");
     }
-    if (encodedMethod.isStatic()) {
+    if (tracedMethod.getAccessFlags().isStatic()) {
       append("static ");
     }
-    printNameAndReturn(encodedMethod);
-    printArguments(encodedMethod.method);
+    printNameAndReturn(tracedMethod.getReference());
+    printArguments(tracedMethod.getReference());
     appendLine(";");
   }
 
diff --git a/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java b/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java
index 12b2e6b..a29fdb5 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java
@@ -3,35 +3,30 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.tracereferences;
 
-import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.StringConsumer;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedClass;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedField;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedMethod;
 import java.util.List;
 
-class PrintUsesFormatter extends ResultFormatter {
-
-  PrintUsesFormatter(StringConsumer output, DiagnosticsHandler diagnosticsHandler) {
-    super(output, diagnosticsHandler);
-  }
+class PrintUsesFormatter extends Formatter {
 
   @Override
-  protected void printConstructorName(DexEncodedMethod encodedMethod) {
-    if (encodedMethod.accessFlags.isStatic()) {
+  protected void printConstructorName(MethodReference method) {
+    if (method.getMethodName().equals("<clinit>")) {
       append("<clinit>");
     } else {
-      String holderName = encodedMethod.holder().toSourceString();
+      String holderName = method.getHolderClass().getTypeName();
       String constructorName = holderName.substring(holderName.lastIndexOf('.') + 1);
       append(constructorName);
     }
   }
 
   @Override
-  protected void printMethod(DexEncodedMethod encodedMethod, String typeName) {
-    append(typeName + ": ");
-    printNameAndReturn(encodedMethod);
-    printArguments(encodedMethod.method);
+  protected void printMethod(TracedMethod method) {
+    append(method.getReference().getHolderClass().getTypeName() + ": ");
+    printNameAndReturn(method.getReference());
+    printArguments(method.getReference());
     appendLine("");
   }
 
@@ -41,20 +36,20 @@
   }
 
   @Override
-  protected void printTypeHeader(DexClass dexClass) {
-    appendLine(dexClass.type.toSourceString());
+  protected void printTypeHeader(TracedClass type) {
+    appendLine(type.getReference().getTypeName());
   }
 
   @Override
   protected void printTypeFooter() {}
 
   @Override
-  protected void printField(DexClass dexClass, DexField field) {
+  protected void printField(TracedField field) {
     appendLine(
-        dexClass.type.toSourceString()
+        field.getReference().getHolderClass().getTypeName()
             + ": "
-            + field.type.toSourceString()
+            + field.getReference().getFieldType().getTypeName()
             + " "
-            + field.name.toString());
+            + field.getReference().getFieldName());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Result.java b/src/main/java/com/android/tools/r8/tracereferences/Result.java
deleted file mode 100644
index 6889f0c..0000000
--- a/src/main/java/com/android/tools/r8/tracereferences/Result.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.graph.DexApplication;
-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 java.util.Map;
-import java.util.Set;
-
-class Result {
-  final DexApplication application;
-  final Set<DexType> types;
-  final Set<String> keepPackageNames;
-  final Map<DexType, Set<DexField>> fields;
-  final Map<DexType, Set<DexMethod>> methods;
-  final Set<DexReference> missingDefinition;
-
-  Result(
-      DexApplication application,
-      Set<DexType> types,
-      Set<String> keepPackageNames,
-      Map<DexType, Set<DexField>> fields,
-      Map<DexType, Set<DexMethod>> methods,
-      Set<DexReference> missingDefinition) {
-    this.application = application;
-    this.types = types;
-    this.keepPackageNames = keepPackageNames;
-    this.fields = fields;
-    this.methods = methods;
-    this.missingDefinition = missingDefinition;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/ResultFormatter.java b/src/main/java/com/android/tools/r8/tracereferences/ResultFormatter.java
deleted file mode 100644
index 2fd8934..0000000
--- a/src/main/java/com/android/tools/r8/tracereferences/ResultFormatter.java
+++ /dev/null
@@ -1,142 +0,0 @@
-// 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.DiagnosticsHandler;
-import com.android.tools.r8.StringConsumer;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
-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 java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-abstract class ResultFormatter {
-
-  private final StringConsumer output;
-  private final DiagnosticsHandler diagnosticsHandler;
-
-  ResultFormatter(StringConsumer output, DiagnosticsHandler diagnosticsHandler) {
-    this.output = output;
-    this.diagnosticsHandler = diagnosticsHandler;
-  }
-
-  protected void append(String string) {
-    output.accept(string, diagnosticsHandler);
-  }
-
-  protected void appendLine(String string) {
-    output.accept(string + System.lineSeparator(), diagnosticsHandler);
-  }
-
-  protected void printArguments(DexMethod method) {
-    append("(");
-    for (int i = 0; i < method.getArity(); i++) {
-      if (i != 0) {
-        append(",");
-      }
-      append(method.proto.parameters.values[i].toSourceString());
-    }
-    append(")");
-  }
-
-  protected abstract void printConstructorName(DexEncodedMethod encodedMethod);
-
-  private void printError(String message) {
-    appendLine("# Error: " + message);
-  }
-
-  protected abstract void printField(DexClass dexClass, DexField field);
-
-  protected abstract void printMethod(DexEncodedMethod encodedMethod, String typeName);
-
-  protected abstract void printPackageNames(List<String> packageNames);
-
-  protected void printNameAndReturn(DexEncodedMethod encodedMethod) {
-    if (encodedMethod.accessFlags.isConstructor()) {
-      printConstructorName(encodedMethod);
-    } else {
-      DexMethod method = encodedMethod.method;
-      append(method.proto.returnType.toSourceString());
-      append(" ");
-      append(method.name.toSourceString());
-    }
-  }
-
-  protected abstract void printTypeHeader(DexClass dexClass);
-
-  protected abstract void printTypeFooter();
-
-  void format(Result result) {
-    int errors =
-        print(
-            result.application,
-            result.types,
-            result.keepPackageNames,
-            result.fields,
-            result.methods,
-            result.missingDefinition);
-    output.finished(diagnosticsHandler);
-    assert errors == result.missingDefinition.size();
-  }
-
-  private int print(
-      DexApplication application,
-      Set<DexType> types,
-      Set<String> keepPackageNames,
-      Map<DexType, Set<DexField>> fields,
-      Map<DexType, Set<DexMethod>> methods,
-      Set<DexReference> missingDefinition) {
-    int errors = 0;
-    List<DexType> sortedTypes = new ArrayList<>(types);
-    sortedTypes.sort(Comparator.comparing(DexType::toSourceString));
-    for (DexType type : sortedTypes) {
-      DexClass dexClass = application.definitionFor(type);
-      if (missingDefinition.contains(type)) {
-        assert dexClass == null;
-        printError("Could not find definition for type " + type.toSourceString());
-        errors++;
-        continue;
-      }
-      printTypeHeader(dexClass);
-      List<DexEncodedMethod> methodDefinitions = new ArrayList<>(methods.size());
-      for (DexMethod method : methods.get(type)) {
-        DexEncodedMethod encodedMethod = dexClass.lookupMethod(method);
-        if (missingDefinition.contains(method)) {
-          assert encodedMethod == null;
-          printError("Could not find definition for method " + method.toSourceString());
-          errors++;
-          continue;
-        }
-        methodDefinitions.add(encodedMethod);
-      }
-      methodDefinitions.sort(Comparator.comparing(x -> x.method.name.toSourceString()));
-      for (DexEncodedMethod encodedMethod : methodDefinitions) {
-        printMethod(encodedMethod, dexClass.type.toSourceString());
-      }
-      List<DexField> sortedFields = new ArrayList<>(fields.get(type));
-      sortedFields.sort(Comparator.comparing(DexField::toSourceString));
-      for (DexField field : sortedFields) {
-        if (missingDefinition.contains(field)) {
-          printError("Could not find definition for field " + field.toSourceString());
-          errors++;
-          continue;
-        }
-        printField(dexClass, field);
-      }
-      printTypeFooter();
-    }
-    ArrayList<String> packageNamesToKeep = new ArrayList<>(keepPackageNames);
-    Collections.sort(packageNamesToKeep);
-    printPackageNames(packageNamesToKeep);
-    return errors;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
index d0ec93e..acc2cb6 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
@@ -32,7 +32,7 @@
       throw new CompilationFailedException();
     } catch (Exception e) {
       command.getDiagnosticsHandler().error(new ExceptionDiagnostic(e));
-      throw new CompilationFailedException();
+      throw new CompilationFailedException(e);
     }
   }
 
@@ -46,8 +46,8 @@
     if (command.getSource().isEmpty()) {
       throw new TraceReferencesException("No source specified");
     }
-    if (command.getOutput() == null) {
-      throw new TraceReferencesException("No output specified");
+    if (command.getConsumer() == null) {
+      throw new TraceReferencesException("No consumer specified");
     }
     AndroidApp.Builder builder = AndroidApp.builder();
     command.getLibrary().forEach(builder::addLibraryResourceProvider);
@@ -80,25 +80,8 @@
         }
       }
     }
-    Tracer tracer = new Tracer(tagetDescriptors, builder.build());
-    Result result = tracer.run();
-    ResultFormatter formatter;
-    switch (command.getOutputFormat()) {
-      case PRINTUSAGE:
-        formatter = new PrintUsesFormatter(command.getOutput(), command.getDiagnosticsHandler());
-        break;
-      case KEEP_RULES:
-        formatter =
-            new KeepRuleFormatter(command.getOutput(), command.getDiagnosticsHandler(), false);
-        break;
-      case KEEP_RULES_WITH_ALLOWOBFUSCATION:
-        formatter =
-            new KeepRuleFormatter(command.getOutput(), command.getDiagnosticsHandler(), true);
-        break;
-      default:
-        throw new TraceReferencesException("Unexpected format " + command.getOutputFormat().name());
-    }
-    formatter.format(result);
+    Tracer tracer = new Tracer(tagetDescriptors, builder.build(), command.getDiagnosticsHandler());
+    tracer.run(command.getConsumer());
   }
 
   public static void run(String... args) throws CompilationFailedException {
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
index f6fcb69..ae0ee57 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
@@ -11,10 +11,8 @@
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.Keep;
 import com.android.tools.r8.ProgramResourceProvider;
-import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
-import com.android.tools.r8.tracereferences.TraceReferencesCommandParser.OutputFormat;
 import com.android.tools.r8.utils.ArchiveResourceProvider;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -35,8 +33,7 @@
   private final ImmutableList<ClassFileResourceProvider> library;
   private final ImmutableList<ClassFileResourceProvider> traceTarget;
   private final ImmutableList<ProgramResourceProvider> traceSource;
-  private final StringConsumer output;
-  private final OutputFormat outputFormat;
+  private final TraceReferencesConsumer consumer;
 
   TraceReferencesCommand(
       boolean printHelp,
@@ -45,16 +42,14 @@
       ImmutableList<ClassFileResourceProvider> library,
       ImmutableList<ClassFileResourceProvider> traceTarget,
       ImmutableList<ProgramResourceProvider> traceSource,
-      StringConsumer output,
-      OutputFormat outputFormat) {
+      TraceReferencesConsumer consumer) {
     this.printHelp = printHelp;
     this.printVersion = printVersion;
     this.diagnosticsHandler = diagnosticsHandler;
     this.library = library;
     this.traceTarget = traceTarget;
     this.traceSource = traceSource;
-    this.output = output;
-    this.outputFormat = outputFormat;
+    this.consumer = consumer;
   }
 
   /**
@@ -101,8 +96,7 @@
         ImmutableList.builder();
     private final ImmutableList.Builder<ProgramResourceProvider> traceSourceBuilder =
         ImmutableList.builder();
-    private StringConsumer output;
-    private OutputFormat outputFormat = TraceReferencesCommandParser.OutputFormat.PRINTUSAGE;
+    private TraceReferencesConsumer consumer;
 
     private Builder(DiagnosticsHandler diagnosticsHandler) {
       this.diagnosticsHandler = diagnosticsHandler;
@@ -197,13 +191,8 @@
       return this;
     }
 
-    Builder setOutputPath(Path output) {
-      this.output = new StringConsumer.FileConsumer(output);
-      return this;
-    }
-
-    Builder setOutputFormat(OutputFormat outputFormat) {
-      this.outputFormat = outputFormat;
+    Builder setConsumer(TraceReferencesConsumer consumer) {
+      this.consumer = consumer;
       return this;
     }
 
@@ -217,8 +206,7 @@
           libraryBuilder.build(),
           traceTarget,
           traceSource,
-          output,
-          outputFormat);
+          consumer);
     }
 
     void error(Diagnostic diagnostic) {
@@ -244,11 +232,7 @@
     return traceSource;
   }
 
-  StringConsumer getOutput() {
-    return output;
-  }
-
-  OutputFormat getOutputFormat() {
-    return outputFormat;
+  TraceReferencesConsumer getConsumer() {
+    return consumer;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
index 79de8e2..476d0d4 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
@@ -6,11 +6,13 @@
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.JdkClassFileProvider;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.tracereferences.TraceReferencesFormattingConsumer.OutputFormat;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -19,17 +21,6 @@
 
 class TraceReferencesCommandParser {
 
-  enum OutputFormat {
-    /** Format used with the -printusage flag */
-    PRINTUSAGE,
-    /** Keep rules keeping each of the traced references */
-    KEEP_RULES,
-    /**
-     * Keep rules with <code>allowobfuscation</code> modifier keeping each of the traced references
-     */
-    KEEP_RULES_WITH_ALLOWOBFUSCATION
-  }
-
   private static final Set<String> OPTIONS_WITH_PARAMETER =
       ImmutableSet.of("--lib", "--target", "--source", "--format", "--output");
 
@@ -78,6 +69,8 @@
   private TraceReferencesCommand.Builder parse(
       String[] args, Origin origin, TraceReferencesCommand.Builder builder) {
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
+    Path output = null;
+    OutputFormat format = TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE;
     for (int i = 0; i < expandedArgs.length; i++) {
       String arg = expandedArgs[i].trim();
       String nextArg = null;
@@ -103,28 +96,42 @@
       } else if (arg.equals("--source")) {
         builder.addSourceFiles(Paths.get(nextArg));
       } else if (arg.equals("--format")) {
-        OutputFormat format = null;
         if (nextArg.equals("printuses")) {
-          format = OutputFormat.PRINTUSAGE;
+          format = TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE;
         }
         if (nextArg.equals("keep")) {
-          format = OutputFormat.KEEP_RULES;
+          format = TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES;
         }
         if (nextArg.equals("keepallowobfuscation")) {
-          format = OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION;
+          format = TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION;
         }
         if (format == null) {
           builder.error(new StringDiagnostic("Unsupported format '" + nextArg + "'"));
         }
-        builder.setOutputFormat(format);
       } else if (arg.equals("--output")) {
-        builder.setOutputPath(Paths.get(nextArg));
+        output = Paths.get(nextArg);
       } else if (arg.startsWith("@")) {
         builder.error(new StringDiagnostic("Recursive @argfiles are not supported: ", origin));
       } else {
         builder.error(new StringDiagnostic("Unsupported argument '" + arg + "'"));
       }
     }
+    final Path finalOutput = output;
+    builder.setConsumer(
+        new TraceReferencesFormattingConsumer(format) {
+          @Override
+          public void finished() {
+            PrintStream out = System.out;
+            if (finalOutput != null) {
+              try {
+                out = new PrintStream(Files.newOutputStream(finalOutput));
+              } catch (IOException e) {
+                builder.error(new ExceptionDiagnostic(e));
+              }
+            }
+            out.print(get());
+          }
+        });
     return builder;
   }
 
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
new file mode 100644
index 0000000..38b8086
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
@@ -0,0 +1,90 @@
+// 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.Keep;
+import com.android.tools.r8.KeepForSubclassing;
+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.PackageReference;
+
+/** Consumer interface for recording references */
+@KeepForSubclassing
+public interface TraceReferencesConsumer {
+
+  /**
+   * Interface for asking for the access flags for a traced reference when the definition is present
+   */
+  @Keep
+  interface AccessFlags {
+    boolean isStatic();
+
+    boolean isPublic();
+
+    boolean isProtected();
+
+    boolean isPrivate();
+  }
+
+  /**
+   * Interface for asking for additional class information for a traced class when the definition is
+   * found.
+   */
+  @Keep
+  interface ClassAccessFlags extends AccessFlags {
+    boolean isInterface();
+
+    boolean isEnum();
+  }
+
+  @Keep
+  interface FieldAccessFlags extends AccessFlags {}
+
+  @Keep
+  interface MethodAccessFlags extends AccessFlags {}
+
+  /** Interface implemented by all references reported */
+  @Keep
+  interface TracedReference<T, F> {
+    /** Returns if the reference does not have a definition in the program traced. */
+    boolean isMissingDefinition();
+
+    /** Returns the reference traced. */
+    T getReference();
+
+    /**
+     * Returns the access flags for the reference traced. If the definition is not found (<code>
+     * isMissingDefinition()</code> returns <code>true</code>) the access flags are not known and
+     * this returns <code>null</code>.
+     */
+    F getAccessFlags();
+  }
+
+  @Keep
+  interface TracedClass extends TracedReference<ClassReference, ClassAccessFlags> {}
+
+  @Keep
+  interface TracedField extends TracedReference<FieldReference, FieldAccessFlags> {}
+
+  @Keep
+  interface TracedMethod extends TracedReference<MethodReference, MethodAccessFlags> {}
+
+  /** Class has been traced. */
+  void acceptType(TracedClass tracedClazz);
+
+  /** Field has been traced. */
+  void acceptField(TracedField tracedField);
+
+  /** Method has been traced. */
+  void acceptMethod(TracedMethod tracedMethod);
+
+  /** Package which is required for package privatge access has been traced. */
+  void acceptPackage(PackageReference pkg);
+
+  /**
+   * Tracing has finished. There will be no more calls to any of the <code>acceptXXX</code> methods.
+   */
+  void finished();
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesFormattingConsumer.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesFormattingConsumer.java
new file mode 100644
index 0000000..67167c4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesFormattingConsumer.java
@@ -0,0 +1,79 @@
+// 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.errors.Unreachable;
+import com.android.tools.r8.references.PackageReference;
+
+class TraceReferencesFormattingConsumer implements TraceReferencesConsumer {
+
+  public enum OutputFormat {
+    /** Format used with the -printusage flag */
+    PRINTUSAGE,
+    /** Keep rules keeping each of the traced references */
+    KEEP_RULES,
+    /**
+     * Keep rules with <code>allowobfuscation</code> modifier keeping each of the traced references
+     */
+    KEEP_RULES_WITH_ALLOWOBFUSCATION
+  }
+
+  private final OutputFormat format;
+  private final TraceReferencesResult.Builder builder = TraceReferencesResult.builder();
+  private boolean finishedCalled = false;
+
+  public TraceReferencesFormattingConsumer(OutputFormat format) {
+    this.format = format;
+  }
+
+  @Override
+  public void acceptType(TracedClass type) {
+    assert !finishedCalled;
+    builder.acceptType(type);
+  }
+
+  @Override
+  public void acceptField(TracedField field) {
+    assert !finishedCalled;
+    builder.acceptField(field);
+  }
+
+  @Override
+  public void acceptMethod(TracedMethod method) {
+    assert !finishedCalled;
+    builder.acceptMethod(method);
+  }
+
+  @Override
+  public void acceptPackage(PackageReference pkg) {
+    assert !finishedCalled;
+    builder.acceptPackage(pkg);
+  }
+
+  @Override
+  public void finished() {
+    assert !finishedCalled;
+    finishedCalled = true;
+  }
+
+  public String get() {
+    TraceReferencesResult result = builder.build();
+    Formatter formatter;
+    switch (format) {
+      case PRINTUSAGE:
+        formatter = new PrintUsesFormatter();
+        break;
+      case KEEP_RULES:
+        formatter = new KeepRuleFormatter(false);
+        break;
+      case KEEP_RULES_WITH_ALLOWOBFUSCATION:
+        formatter = new KeepRuleFormatter(true);
+        break;
+      default:
+        throw new Unreachable();
+    }
+    formatter.format(result);
+    return formatter.get();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java
new file mode 100644
index 0000000..f8dab44
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java
@@ -0,0 +1,94 @@
+// 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.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.PackageReference;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedClass;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedField;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedMethod;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+class TraceReferencesResult {
+
+  final Set<TracedClass> types;
+  final Map<ClassReference, Set<TracedField>> fields;
+  final Map<ClassReference, Set<TracedMethod>> methods;
+  final Set<PackageReference> keepPackageNames;
+  final Set<Object> missingDefinition;
+
+  TraceReferencesResult(
+      Set<TracedClass> types,
+      Map<ClassReference, Set<TracedField>> fields,
+      Map<ClassReference, Set<TracedMethod>> methods,
+      Set<PackageReference> keepPackageNames,
+      Set<Object> missingDefinition) {
+    this.types = types;
+    this.fields = fields;
+    this.methods = methods;
+    this.keepPackageNames = keepPackageNames;
+    this.missingDefinition = missingDefinition;
+  }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder implements TraceReferencesConsumer {
+    private final Set<TracedClass> types = new HashSet<>();
+    private final Map<ClassReference, Set<TracedField>> fields = new HashMap<>();
+    private final Map<ClassReference, Set<TracedMethod>> methods = new HashMap<>();
+    private final Set<Object> missingDefinition = new HashSet<>();
+    private final Set<PackageReference> keepPackageNames = new HashSet<>();
+
+    @Override
+    public void acceptType(TracedClass tracedClass) {
+      types.add(tracedClass);
+      if (tracedClass.isMissingDefinition()) {
+        this.missingDefinition.add(tracedClass.getReference());
+      }
+    }
+
+    @Override
+    public void acceptField(TracedField tracedField) {
+      FieldReference field = tracedField.getReference();
+      fields.computeIfAbsent(field.getHolderClass(), k -> new HashSet<>()).add(tracedField);
+      if (tracedField.isMissingDefinition()) {
+        this.missingDefinition.add(field);
+      }
+    }
+
+    @Override
+    public void acceptMethod(TracedMethod tracedMethod) {
+      MethodReference method = tracedMethod.getReference();
+      methods.computeIfAbsent(method.getHolderClass(), k -> new HashSet<>()).add(tracedMethod);
+      if (tracedMethod.isMissingDefinition()) {
+        this.missingDefinition.add(method);
+      }
+    }
+
+    @Override
+    public void acceptPackage(PackageReference pkg) {
+      keepPackageNames.add(pkg);
+    }
+
+    @Override
+    public void finished() {}
+
+    TraceReferencesResult build() {
+      missingDefinition.forEach(
+          missingDefinition -> {
+            assert missingDefinition instanceof ClassReference
+                || missingDefinition instanceof FieldReference
+                || missingDefinition instanceof MethodReference;
+          });
+      return new TraceReferencesResult(types, fields, methods, keepPackageNames, missingDefinition);
+    }
+  }
+}
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 5ad1cfa..d30158b 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -17,7 +18,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexValueArray;
@@ -26,29 +26,209 @@
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
+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.android.tools.r8.shaking.MainDexClasses;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.AccessFlags;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.ClassAccessFlags;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.FieldAccessFlags;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.MethodAccessFlags;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedClass;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedField;
+import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedMethod;
+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 com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 class Tracer {
 
+  static class AccessFlagsImpl<T extends com.android.tools.r8.graph.AccessFlags<T>>
+      implements AccessFlags {
+    T accessFlags;
+
+    AccessFlagsImpl(T accessFlags) {
+      this.accessFlags = accessFlags;
+    }
+
+    @Override
+    public boolean isStatic() {
+      return accessFlags.isStatic();
+    }
+
+    @Override
+    public boolean isPublic() {
+      return accessFlags.isPublic();
+    }
+
+    @Override
+    public boolean isProtected() {
+      return accessFlags.isProtected();
+    }
+
+    @Override
+    public boolean isPrivate() {
+      return accessFlags.isPrivate();
+    }
+  }
+
+  static class ClassAccessFlagsImpl
+      extends AccessFlagsImpl<com.android.tools.r8.graph.ClassAccessFlags>
+      implements ClassAccessFlags {
+    ClassAccessFlagsImpl(com.android.tools.r8.graph.ClassAccessFlags accessFlags) {
+      super(accessFlags);
+    }
+
+    @Override
+    public boolean isInterface() {
+      return accessFlags.isInterface();
+    }
+
+    @Override
+    public boolean isEnum() {
+      return accessFlags.isEnum();
+    }
+  }
+
+  static class FieldAccessFlagsImpl
+      extends AccessFlagsImpl<com.android.tools.r8.graph.FieldAccessFlags>
+      implements FieldAccessFlags {
+    FieldAccessFlagsImpl(com.android.tools.r8.graph.FieldAccessFlags accessFlags) {
+      super(accessFlags);
+    }
+  }
+
+  static class MethodAccessFlagsImpl
+      extends AccessFlagsImpl<com.android.tools.r8.graph.MethodAccessFlags>
+      implements MethodAccessFlags {
+    MethodAccessFlagsImpl(com.android.tools.r8.graph.MethodAccessFlags accessFlags) {
+      super(accessFlags);
+    }
+  }
+
+  abstract static class TracedReferenceBase<T, F> implements TracedReference<T, F> {
+    private final T reference;
+    private final F accessFlags;
+    private final boolean missingDefinition;
+
+    private TracedReferenceBase(T reference, F accessFlags, boolean missingDefinition) {
+      assert accessFlags != null || missingDefinition;
+      this.reference = reference;
+      this.accessFlags = accessFlags;
+      this.missingDefinition = missingDefinition;
+    }
+
+    @Override
+    public T getReference() {
+      return reference;
+    }
+
+    @Override
+    public boolean isMissingDefinition() {
+      return missingDefinition;
+    }
+
+    @Override
+    public F getAccessFlags() {
+      return accessFlags;
+    }
+
+    @Override
+    public int hashCode() {
+      // Equality is only based on the reference.
+      return reference.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      // Equality is only based on the reference.
+      if (!(other instanceof TracedReferenceBase)) {
+        return false;
+      }
+      return reference.equals(((TracedReferenceBase<?, ?>) other).reference);
+    }
+
+    public abstract String getKindName();
+  }
+
+  static class TracedClassImpl extends TracedReferenceBase<ClassReference, ClassAccessFlags>
+      implements TracedClass {
+    private TracedClassImpl(DexType reference, DexClass definition) {
+      super(
+          Reference.classFromDescriptor(reference.toDescriptorString()),
+          definition != null ? new ClassAccessFlagsImpl(definition.getAccessFlags()) : null,
+          definition == null);
+    }
+
+    @Override
+    public String getKindName() {
+      return "type";
+    }
+
+    @Override
+    public String toString() {
+      return getReference().getTypeName();
+    }
+  }
+
+  static class TracedFieldImpl extends TracedReferenceBase<FieldReference, FieldAccessFlags>
+      implements TracedField {
+    private TracedFieldImpl(DexField reference, DexEncodedField definition) {
+      super(
+          Reference.field(
+              Reference.classFromDescriptor(reference.holder.toDescriptorString()),
+              reference.name.toString(),
+              Reference.typeFromDescriptor(reference.type.toDescriptorString())),
+          definition != null ? new FieldAccessFlagsImpl(definition.getAccessFlags()) : null,
+          definition == null);
+    }
+
+    @Override
+    public String getKindName() {
+      return "field";
+    }
+
+    @Override
+    public String toString() {
+      return getReference().toString();
+    }
+  }
+
+  static class TracedMethodImpl extends TracedReferenceBase<MethodReference, MethodAccessFlags>
+      implements TracedMethod {
+    private TracedMethodImpl(DexMethod reference, DexEncodedMethod definition) {
+      super(
+          reference.asMethodReference(),
+          definition != null ? new MethodAccessFlagsImpl(definition.getAccessFlags()) : null,
+          definition == null);
+    }
+
+    @Override
+    public String getKindName() {
+      return "method";
+    }
+
+    @Override
+    public String toString() {
+      return getReference().toString();
+    }
+  }
+
   private final Set<String> descriptors;
-  private Set<DexType> types = Sets.newIdentityHashSet();
-  private Map<DexType, Set<DexMethod>> methods = Maps.newIdentityHashMap();
-  private Map<DexType, Set<DexField>> fields = Maps.newIdentityHashMap();
-  private Set<String> keepPackageNames = Sets.newHashSet();
-  private Set<DexReference> missingDefinitions = Sets.newHashSet();
+  private final DiagnosticsHandler diagnostics;
   private final DirectMappedDexApplication application;
   private final AppInfoWithClassHierarchy appInfo;
 
-  Tracer(Set<String> descriptors, AndroidApp inputApp) throws Exception {
+  Tracer(Set<String> descriptors, AndroidApp inputApp, DiagnosticsHandler diagnostics)
+      throws Exception {
     this.descriptors = descriptors;
+    this.diagnostics = diagnostics;
     InternalOptions options = new InternalOptions();
     application =
         new ApplicationReader(inputApp, options, new Timing("ReferenceTrace")).read().toDirect();
@@ -59,8 +239,8 @@
             MainDexClasses.createEmptyMainDexClasses());
   }
 
-  Result run() {
-    UseCollector useCollector = new UseCollector(appInfo.dexItemFactory());
+  void run(TraceReferencesConsumer consumer) {
+    UseCollector useCollector = new UseCollector(appInfo.dexItemFactory(), consumer, diagnostics);
     for (DexProgramClass clazz : application.classes()) {
       useCollector.setContext(clazz);
       useCollector.registerSuperType(clazz, clazz.superType);
@@ -70,74 +250,80 @@
       clazz.forEachProgramMethod(useCollector::registerMethod);
       clazz.forEachField(useCollector::registerField);
     }
-
-    return new Result(application, types, keepPackageNames, fields, methods, missingDefinitions);
-  }
-
-  private boolean isTargetType(DexType type) {
-    return descriptors.contains(type.toDescriptorString());
-  }
-
-  private void addType(DexType type) {
-    if (isTargetType(type) && types.add(type)) {
-      DexClass clazz = appInfo.definitionFor(type);
-      if (clazz != null && clazz.accessFlags.isVisibilityDependingOnPackage()) {
-        keepPackageNames.add(clazz.type.getPackageName());
-      }
-      methods.put(type, Sets.newIdentityHashSet());
-      fields.put(type, Sets.newIdentityHashSet());
-    }
-  }
-
-  private void addField(DexField field) {
-    addType(field.type);
-    DexEncodedField baseField = appInfo.resolveField(field).getResolvedField();
-    if (baseField != null && baseField.holder() != field.holder) {
-      field = baseField.field;
-    }
-    addType(field.holder);
-    if (isTargetType(field.holder)) {
-      Set<DexField> typeFields = fields.get(field.holder);
-      assert typeFields != null;
-      if (baseField != null) {
-        if (baseField.accessFlags.isVisibilityDependingOnPackage()) {
-          keepPackageNames.add(baseField.holder().getPackageName());
-        }
-      } else {
-        missingDefinitions.add(field);
-      }
-      typeFields.add(field);
-    }
-  }
-
-  private void addMethod(DexMethod method) {
-    addType(method.holder);
-    for (DexType parameterType : method.proto.parameters.values) {
-      addType(parameterType);
-    }
-    addType(method.proto.returnType);
-    if (isTargetType(method.holder)) {
-      Set<DexMethod> typeMethods = methods.get(method.holder);
-      assert typeMethods != null;
-      DexClass holder = appInfo.definitionForHolder(method);
-      DexEncodedMethod definition = method.lookupOnClass(holder);
-      if (definition != null) {
-        if (definition.accessFlags.isVisibilityDependingOnPackage()) {
-          keepPackageNames.add(definition.holder().getPackageName());
-        }
-      } else {
-        missingDefinitions.add(method);
-      }
-      typeMethods.add(method);
-    }
+    consumer.finished();
   }
 
   class UseCollector extends UseRegistry {
 
+    private final TraceReferencesConsumer consumer;
     private DexProgramClass context;
+    private final DiagnosticsHandler diagnostics;
+    private Set<TracedReference<?, ?>> missingDefinitionReported = new HashSet<>();
 
-    UseCollector(DexItemFactory factory) {
+    UseCollector(
+        DexItemFactory factory, TraceReferencesConsumer consumer, DiagnosticsHandler diagnostics) {
       super(factory);
+      this.consumer = consumer;
+      this.diagnostics = diagnostics;
+    }
+
+    private boolean isTargetType(DexType type) {
+      return descriptors.contains(type.toDescriptorString());
+    }
+
+    private void addType(DexType type) {
+      if (isTargetType(type)) {
+        DexClass clazz = appInfo.definitionFor(type);
+        TracedClassImpl tracedClass = new TracedClassImpl(type, clazz);
+        consumer.acceptType(tracedClass);
+        checkDiagnostics(tracedClass);
+        if (clazz != null && clazz.accessFlags.isVisibilityDependingOnPackage()) {
+          consumer.acceptPackage(Reference.packageFromString(clazz.type.getPackageName()));
+        }
+      }
+    }
+
+    private void addField(DexField field) {
+      addType(field.type);
+      DexEncodedField baseField = appInfo.resolveField(field).getResolvedField();
+      if (baseField != null && baseField.holder() != field.holder) {
+        field = baseField.field;
+      }
+      addType(field.holder);
+      if (isTargetType(field.holder)) {
+        TracedFieldImpl tracedField = new TracedFieldImpl(field, baseField);
+        consumer.acceptField(tracedField);
+        checkDiagnostics(tracedField);
+        if (baseField != null && baseField.accessFlags.isVisibilityDependingOnPackage()) {
+          consumer.acceptPackage(Reference.packageFromString(baseField.holder().getPackageName()));
+        }
+      }
+    }
+
+    private void addMethod(DexMethod method) {
+      addType(method.holder);
+      for (DexType parameterType : method.proto.parameters.values) {
+        addType(parameterType);
+      }
+      addType(method.proto.returnType);
+      if (isTargetType(method.holder)) {
+        DexClass holder = appInfo.definitionForHolder(method);
+        DexEncodedMethod definition = method.lookupOnClass(holder);
+        TracedMethodImpl tracedMethod = new TracedMethodImpl(method, definition);
+        consumer.acceptMethod(tracedMethod);
+        checkDiagnostics(tracedMethod);
+        if (definition != null && definition.accessFlags.isVisibilityDependingOnPackage()) {
+          consumer.acceptPackage(Reference.packageFromString(definition.holder().getPackageName()));
+        }
+      }
+    }
+
+    private void checkDiagnostics(TracedReferenceBase<?, ?> tracedReference) {
+      if (tracedReference.isMissingDefinition() && missingDefinitionReported.add(tracedReference)) {
+        diagnostics.warning(
+            new StringDiagnostic(
+                "Missing definition of " + tracedReference.getKindName() + " " + tracedReference));
+      }
     }
 
     public void setContext(DexProgramClass context) {
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 489d8b2..88ec578 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
@@ -14,7 +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.tracereferences.TraceReferencesCommandParser.OutputFormat;
+import com.android.tools.r8.tracereferences.TraceReferencesFormattingConsumer.OutputFormat;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
@@ -26,6 +26,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
@@ -53,8 +54,7 @@
     assertEquals(0, command.getLibrary().size());
     assertEquals(0, command.getTarget().size());
     assertEquals(0, command.getSource().size());
-    assertEquals(TraceReferencesCommandParser.OutputFormat.PRINTUSAGE, command.getOutputFormat());
-    assertNull(command.getOutput());
+    assertNull(command.getConsumer());
   }
 
   @Test(expected = CompilationFailedException.class)
@@ -105,29 +105,48 @@
   }
 
   private String formatName(OutputFormat format) {
-    if (format == TraceReferencesCommandParser.OutputFormat.PRINTUSAGE) {
+    if (format == TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE) {
       return "printuses";
     }
-    if (format == TraceReferencesCommandParser.OutputFormat.KEEP_RULES) {
+    if (format == TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES) {
       return "keep";
     }
-    assertSame(format, TraceReferencesCommandParser.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION);
+    assertSame(
+        format, TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION);
     return "keepallowobfuscation";
   }
 
   public void runAndCheckOutput(
       Path targetJar, Path sourceJar, OutputFormat format, String expected) throws Throwable {
+    runAndCheckOutput(targetJar, sourceJar, format, expected, null);
+  }
+
+  public void runAndCheckOutput(
+      Path targetJar,
+      Path sourceJar,
+      OutputFormat format,
+      String expected,
+      Consumer<DiagnosticsChecker> diagnosticsCheckerConsumer)
+      throws Throwable {
     Path dir = temp.newFolder().toPath();
     Path output = dir.resolve("output.txt");
+    DiagnosticsChecker diagnosticsChecker = new DiagnosticsChecker();
+    TraceReferencesFormattingConsumer consumer = new TraceReferencesFormattingConsumer(format);
     TraceReferences.run(
-        TraceReferencesCommand.builder()
+        TraceReferencesCommand.builder(diagnosticsChecker)
             .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
             .addTargetFiles(targetJar)
             .addSourceFiles(sourceJar)
-            .setOutputPath(output)
-            .setOutputFormat(format)
+            .setConsumer(consumer)
             .build());
-    assertEquals(expected, FileUtils.readTextFile(output, Charsets.UTF_8));
+    assertEquals(expected, consumer.get());
+    if (diagnosticsCheckerConsumer != null) {
+      diagnosticsCheckerConsumer.accept(diagnosticsChecker);
+    } else {
+      assertEquals(0, diagnosticsChecker.errors.size());
+      assertEquals(0, diagnosticsChecker.warnings.size());
+      assertEquals(0, diagnosticsChecker.infos.size());
+    }
 
     TraceReferences.run(
         TraceReferencesCommand.parse(
@@ -172,7 +191,7 @@
     runAndCheckOutput(
         ImmutableList.of(Target.class),
         ImmutableList.of(Source.class),
-        TraceReferencesCommandParser.OutputFormat.PRINTUSAGE,
+        TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE,
         StringUtils.lines(
             ImmutableList.of(
                 "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
@@ -187,7 +206,7 @@
     runAndCheckOutput(
         ImmutableList.of(Target.class),
         ImmutableList.of(Source.class),
-        TraceReferencesCommandParser.OutputFormat.KEEP_RULES,
+        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES,
         StringUtils.lines(
             ImmutableList.of(
                 "-keep class com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
@@ -203,7 +222,7 @@
     runAndCheckOutput(
         ImmutableList.of(Target.class),
         ImmutableList.of(Source.class),
-        TraceReferencesCommandParser.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
+        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
         StringUtils.lines(
             ImmutableList.of(
                 "-keep,allowobfuscation class"
@@ -219,7 +238,7 @@
     runAndCheckOutput(
         ImmutableList.of(OtherTarget.class),
         ImmutableList.of(Source.class),
-        TraceReferencesCommandParser.OutputFormat.PRINTUSAGE,
+        TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE,
         StringUtils.lines(ImmutableList.of()));
   }
 
@@ -228,7 +247,7 @@
     runAndCheckOutput(
         ImmutableList.of(OtherTarget.class),
         ImmutableList.of(Source.class),
-        TraceReferencesCommandParser.OutputFormat.KEEP_RULES,
+        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES,
         StringUtils.lines(ImmutableList.of()));
   }
 
@@ -237,7 +256,7 @@
     runAndCheckOutput(
         ImmutableList.of(OtherTarget.class),
         ImmutableList.of(Source.class),
-        TraceReferencesCommandParser.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
+        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
         StringUtils.lines(ImmutableList.of()));
   }
 
@@ -264,7 +283,7 @@
     runAndCheckOutput(
         targetJar,
         sourceJar,
-        TraceReferencesCommandParser.OutputFormat.PRINTUSAGE,
+        TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE,
         StringUtils.lines(
             ImmutableList.of(
                 "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
@@ -273,7 +292,12 @@
                     + ".target(int)",
                 "# Error: Could not find definition for field int"
                     + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
-                    + ".field")));
+                    + ".field")),
+        diagnosticsChecker -> {
+          assertEquals(0, diagnosticsChecker.errors.size());
+          assertEquals(2, diagnosticsChecker.warnings.size());
+          assertEquals(0, diagnosticsChecker.infos.size());
+        });
   }
 
   @Test
@@ -289,7 +313,7 @@
     runAndCheckOutput(
         targetJar,
         sourceJar,
-        TraceReferencesCommandParser.OutputFormat.KEEP_RULES,
+        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES,
         StringUtils.lines(
             ImmutableList.of(
                 "-keep class com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
@@ -301,7 +325,12 @@
                     + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target"
                     + ".field",
                 "}",
-                "-keeppackagenames com.android.tools.r8.tracereferences")));
+                "-keeppackagenames com.android.tools.r8.tracereferences")),
+        diagnosticsChecker -> {
+          assertEquals(0, diagnosticsChecker.errors.size());
+          assertEquals(2, diagnosticsChecker.warnings.size());
+          assertEquals(0, diagnosticsChecker.infos.size());
+        });
   }
 
   @Test
@@ -317,7 +346,7 @@
     runAndCheckOutput(
         targetJar,
         sourceJar,
-        TraceReferencesCommandParser.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
+        TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION,
         StringUtils.lines(
             ImmutableList.of(
                 "-keep,allowobfuscation class"
@@ -327,7 +356,12 @@
                 "# Error: Could not find definition for field int"
                     + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target.field",
                 "}",
-                "-keeppackagenames com.android.tools.r8.tracereferences")));
+                "-keeppackagenames com.android.tools.r8.tracereferences")),
+        diagnosticsChecker -> {
+          assertEquals(0, diagnosticsChecker.errors.size());
+          assertEquals(2, diagnosticsChecker.warnings.size());
+          assertEquals(0, diagnosticsChecker.infos.size());
+        });
   }
 
   private byte[] getClassWithTargetRemoved() throws IOException {