Add generation of desugared APIs documentation

This code has been used to generate
https://developer.android.com/studio/write/java8-support-table.

With the previous change the generated APIs documentation will not
include methods which are not implemented by the desugared library.

Bug: 171971808
Change-Id: I5e3f2e3bc2888cd4643e0a897511ebede4e94bce
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index 44a8d6e..8e7f1e6 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -13,22 +13,26 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.CfCode.LocalVariableInfo;
+import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProgramClass.ChecksumSupplier;
+import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.LazyLoadedDexApplication;
+import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
@@ -45,6 +49,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
 import java.io.File;
+import java.io.PrintStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -58,6 +63,7 @@
 import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Predicate;
+import java.util.stream.StreamSupport;
 
 public class GenerateLintFiles {
 
@@ -208,8 +214,10 @@
   private SupportedMethods collectSupportedMethods(
       AndroidApiLevel compilationApiLevel, Predicate<DexEncodedMethod> supported) throws Exception {
 
+    // Read the android.jar for the compilation API level. Read it as program instead of library
+    // to get the local information for parameter names.
     AndroidApp library =
-        AndroidApp.builder().addLibraryFiles(getAndroidJarPath(compilationApiLevel)).build();
+        AndroidApp.builder().addProgramFiles(getAndroidJarPath(compilationApiLevel)).build();
     DirectMappedDexApplication dexApplication =
         new ApplicationReader(library, options, Timing.empty()).read().toDirect();
 
@@ -221,7 +229,7 @@
     // Collect all the methods that the library desugar configuration adds support for.
     Set<DexClass> classesWithAllMethodsSupported = Sets.newIdentityHashSet();
     Map<DexClass, List<DexEncodedMethod>> supportedMethods = new LinkedHashMap<>();
-    for (DexLibraryClass clazz : dexApplication.libraryClasses()) {
+    for (DexProgramClass clazz : dexApplication.classes()) {
       String className = clazz.toSourceString();
       // All the methods with the rewritten prefix are supported.
       for (String prefix : desugaredLibraryConfiguration.getRewritePrefix().keySet()) {
@@ -400,13 +408,392 @@
         });
   }
 
-  public static void main(String[] args) throws Exception {
-    if (args.length != 3) {
-      System.out.println(
-          "Usage: GenerateLineFiles <desugar configuration> <desugar implementation> <output"
-              + " directory>");
-      System.exit(1);
+  private static class StringBuilderWithIndent {
+    String NL = System.lineSeparator();
+    StringBuilder builder = new StringBuilder();
+    String indent = "";
+
+    StringBuilderWithIndent() {}
+
+    StringBuilderWithIndent indent(String indent) {
+      this.indent = indent;
+      return this;
     }
-    new GenerateLintFiles(args[0], args[1], args[2]).run();
+
+    StringBuilderWithIndent appendLine(String line) {
+      builder.append(indent);
+      builder.append(line);
+      builder.append(NL);
+      return this;
+    }
+
+    StringBuilderWithIndent emptyLine() {
+      builder.append(NL);
+      return this;
+    }
+
+    public String toString() {
+      return builder.toString();
+    }
+  }
+
+  private abstract static class SourceBuilder<B extends SourceBuilder> {
+
+    protected final DexClass clazz;
+    protected final boolean newClass;
+    protected List<DexEncodedField> fields = new ArrayList<>();
+    protected List<DexEncodedMethod> constructors = new ArrayList<>();
+    protected List<DexEncodedMethod> methods = new ArrayList<>();
+
+    String className;
+    String packageName;
+
+    private SourceBuilder(DexClass clazz, boolean newClass) {
+      this.clazz = clazz;
+      this.newClass = newClass;
+      this.className = clazz.type.toSourceString();
+      int index = this.className.lastIndexOf('.');
+      this.packageName = index > 0 ? this.className.substring(0, index) : "";
+    }
+
+    public abstract B self();
+
+    private B addField(DexEncodedField field) {
+      fields.add(field);
+      return self();
+    }
+
+    private B addMethod(DexEncodedMethod method) {
+      assert !method.isClassInitializer();
+      if (method.isInitializer()) {
+        constructors.add(method);
+      } else {
+        methods.add(method);
+      }
+      return self();
+    }
+
+    protected String typeInPackage(String packageName, String typeName) {
+      if (typeName.startsWith(packageName)
+          && typeName.length() > packageName.length()
+          && typeName.charAt(packageName.length()) == '.'
+          && typeName.indexOf('.', packageName.length() + 1) == -1) {
+        return typeName.substring(packageName.length() + 1);
+      }
+      return null;
+    }
+
+    protected String typeInPackage(String typeName) {
+      String result = typeInPackage(packageName, typeName);
+      if (result == null) {
+        result = typeInPackage("java.lang", typeName);
+      }
+      if (result == null) {
+        result = typeName;
+      }
+      return result.replace('$', '.');
+    }
+
+    protected String typeInPackage(DexType type) {
+      if (type.isPrimitiveType()) {
+        return type.toSourceString();
+      }
+      return typeInPackage(type.toSourceString());
+    }
+
+    protected String accessFlags(ClassAccessFlags accessFlags) {
+      List<String> flags = new ArrayList<>();
+      if (accessFlags.isPublic()) {
+        flags.add("public");
+      }
+      if (accessFlags.isProtected()) {
+        flags.add("protected");
+      }
+      if (accessFlags.isPrivate()) {
+        assert false;
+        flags.add("private");
+      }
+      if (accessFlags.isPackagePrivate()) {
+        assert false;
+        flags.add("/* package */");
+      }
+      if (accessFlags.isAbstract() && !accessFlags.isInterface()) {
+        flags.add("abstract");
+      }
+      if (accessFlags.isStatic()) {
+        flags.add("static");
+      }
+      if (accessFlags.isFinal()) {
+        flags.add("final");
+      }
+      return String.join(" ", flags);
+    }
+
+    protected String accessFlags(FieldAccessFlags accessFlags) {
+      List<String> flags = new ArrayList<>();
+      if (accessFlags.isPublic()) {
+        flags.add("public");
+      }
+      if (accessFlags.isProtected()) {
+        flags.add("protected");
+      }
+      if (accessFlags.isPrivate()) {
+        assert false;
+        flags.add("private");
+      }
+      if (accessFlags.isPackagePrivate()) {
+        assert false;
+        flags.add("/* package */");
+      }
+      if (accessFlags.isStatic()) {
+        flags.add("static");
+      }
+      if (accessFlags.isFinal()) {
+        flags.add("final");
+      }
+      return String.join(" ", flags);
+    }
+
+    protected String accessFlags(MethodAccessFlags accessFlags) {
+      List<String> flags = new ArrayList<>();
+      if (accessFlags.isPublic()) {
+        flags.add("public");
+      }
+      if (accessFlags.isProtected()) {
+        flags.add("protected");
+      }
+      if (accessFlags.isPrivate()) {
+        assert false;
+        flags.add("private");
+      }
+      if (accessFlags.isPackagePrivate()) {
+        assert false;
+        flags.add("/* package */");
+      }
+      if (accessFlags.isAbstract()) {
+        flags.add("abstract");
+      }
+      if (accessFlags.isStatic()) {
+        flags.add("static");
+      }
+      if (accessFlags.isFinal()) {
+        flags.add("final");
+      }
+      return String.join(" ", flags);
+    }
+
+    public String arguments(DexEncodedMethod method) {
+      DexProto proto = method.method.proto;
+      StringBuilder argsBuilder = new StringBuilder();
+      boolean firstArg = true;
+      int argIndex = method.isVirtualMethod() || method.accessFlags.isConstructor() ? 1 : 0;
+      int argNumber = 0;
+      argsBuilder.append("(");
+      for (DexType type : proto.parameters.values) {
+        if (!firstArg) {
+          argsBuilder.append(", ");
+        }
+        if (method.hasCode()) {
+          String name = "p" + argNumber;
+          for (LocalVariableInfo localVariable : method.getCode().asCfCode().getLocalVariables()) {
+            if (localVariable.getIndex() == argIndex) {
+              assert !localVariable.getLocal().name.toString().equals("this");
+              name = localVariable.getLocal().name.toString();
+            }
+          }
+          argsBuilder.append(typeInPackage(type)).append(" ").append(name);
+        } else {
+          argsBuilder.append(typeInPackage(type)).append(" p").append(argNumber);
+        }
+        firstArg = false;
+        argIndex += type.isWideType() ? 2 : 1;
+        argNumber++;
+      }
+      argsBuilder.append(")");
+      return argsBuilder.toString();
+    }
+  }
+
+  private static class HTMLBuilder extends StringBuilderWithIndent {
+    private String indent = "";
+
+    private void increaseIndent() {
+      indent += "  ";
+      indent(indent);
+    }
+
+    private void decreaseIndent() {
+      indent = indent.substring(0, indent.length() - 2);
+      indent(indent);
+    }
+
+    HTMLBuilder appendTdCode(String s) {
+      appendLine("<td><code>" + s + "</code></td>");
+      return this;
+    }
+
+    HTMLBuilder appendTdP(String s) {
+      appendLine("<td><p>" + s + "</p></td>");
+      return this;
+    }
+
+    HTMLBuilder appendLiCode(String s) {
+      appendLine("<li><code>" + s + "</code></li>");
+      return this;
+    }
+
+    HTMLBuilder start(String tag) {
+      appendLine("<" + tag + ">");
+      increaseIndent();
+      return this;
+    }
+
+    HTMLBuilder end(String tag) {
+      decreaseIndent();
+      appendLine("</" + tag + ">");
+      return this;
+    }
+  }
+
+  public static class HTMLSourceBuilder extends SourceBuilder<HTMLSourceBuilder> {
+    private final Set<DexMethod> parallelMethods;
+
+    public HTMLSourceBuilder(DexClass clazz, boolean newClass, Set<DexMethod> parallelMethods) {
+      super(clazz, newClass);
+      this.parallelMethods = parallelMethods;
+    }
+
+    public HTMLSourceBuilder self() {
+      return this;
+    }
+
+    public String toString() {
+      HTMLBuilder builder = new HTMLBuilder();
+      builder.start("tr");
+      if (packageName.length() > 0) {
+        builder.appendTdCode(packageName);
+      }
+      builder.appendTdCode(typeInPackage(className));
+      builder.start("td").start("ul");
+      if (!fields.isEmpty()) {
+        assert newClass; // Currently no fields are added to existing classes.
+        for (DexEncodedField field : fields) {
+          builder.appendLiCode(
+              accessFlags(field.accessFlags)
+                  + " "
+                  + typeInPackage(field.field.type)
+                  + " "
+                  + field.field.name);
+        }
+      }
+      if (!constructors.isEmpty()) {
+        for (DexEncodedMethod constructor : constructors) {
+          builder.appendLiCode(
+              accessFlags(constructor.accessFlags)
+                  + " "
+                  + typeInPackage(className)
+                  + arguments(constructor));
+        }
+      }
+      List<String> parallelM = new ArrayList<>();
+      if (!methods.isEmpty()) {
+        for (DexEncodedMethod method : methods) {
+          builder.appendLiCode(
+              accessFlags(method.accessFlags)
+                  + " "
+                  + typeInPackage(method.method.proto.returnType)
+                  + " "
+                  + method.method.name
+                  + arguments(method));
+          if (parallelMethods.contains(method.method)) {
+            parallelM.add(method.method.name.toString());
+          }
+        }
+      }
+      builder.end("ul").end("td");
+      StringBuilder commentBuilder = new StringBuilder();
+      if (newClass) {
+        commentBuilder.append("Fully implemented class.");
+      } else {
+        commentBuilder.append("Additional methods on existing class.");
+      }
+      if (!parallelM.isEmpty()) {
+        commentBuilder.append(newClass ? "" : "<br>");
+        if (parallelM.size() == 1) {
+          commentBuilder
+              .append("The method <code>")
+              .append(parallelM.get(0))
+              .append("</code> is only supported from API level 21.");
+        } else {
+          commentBuilder.append("The following methods are only supported from API level 21:<br>");
+          for (int i = 0; i < parallelM.size(); i++) {
+            commentBuilder.append("<code>").append(parallelM.get(i)).append("</code><br>");
+          }
+        }
+      }
+      builder.appendTdP(commentBuilder.toString());
+      builder.end("tr");
+      return builder.toString();
+    }
+  }
+
+  private void generateClassHTML(
+      PrintStream ps,
+      DexClass clazz,
+      boolean newClass,
+      Predicate<DexEncodedField> fieldsFilter,
+      Predicate<DexEncodedMethod> methodsFilter) {
+    SourceBuilder builder = new HTMLSourceBuilder(clazz, newClass, parallelMethods);
+    StreamSupport.stream(clazz.fields().spliterator(), false)
+        .filter(fieldsFilter)
+        .filter(field -> field.accessFlags.isPublic() || field.accessFlags.isProtected())
+        .sorted(Comparator.comparing(DexEncodedField::toSourceString))
+        .forEach(builder::addField);
+    StreamSupport.stream(clazz.methods().spliterator(), false)
+        .filter(methodsFilter)
+        .filter(
+            method ->
+                (method.accessFlags.isPublic() || method.accessFlags.isProtected())
+                    && !method.accessFlags.isBridge())
+        .sorted(Comparator.comparing(DexEncodedMethod::toSourceString))
+        .forEach(builder::addMethod);
+    ps.println(builder);
+  }
+
+  private void generateDesugaredLibraryApisDocumetation() throws Exception {
+    PrintStream ps = new PrintStream(Files.newOutputStream(outputDirectory.resolve("apis.html")));
+    // Full classes added.
+    SupportedMethods supportedMethods = collectSupportedMethods(AndroidApiLevel.Q, x -> true);
+    supportedMethods.classesWithAllMethodsSupported.stream()
+        .sorted(Comparator.comparing(clazz -> clazz.type.toSourceString()))
+        .forEach(clazz -> generateClassHTML(ps, clazz, true, field -> true, method -> true));
+
+    // Methods added to existing classes.
+    supportedMethods.supportedMethods.keySet().stream()
+        .filter(clazz -> !supportedMethods.classesWithAllMethodsSupported.contains(clazz))
+        .sorted(Comparator.comparing(clazz -> clazz.type.toSourceString()))
+        .forEach(
+            clazz ->
+                generateClassHTML(
+                    ps,
+                    clazz,
+                    false,
+                    field -> false,
+                    method -> supportedMethods.supportedMethods.get(clazz).contains(method)));
+  }
+
+  public static void main(String[] args) throws Exception {
+    if (args.length == 3) {
+      new GenerateLintFiles(args[0], args[1], args[2]).run();
+      return;
+    }
+    if (args.length == 4 && args[0].equals("--generate-api-docs")) {
+      new GenerateLintFiles(args[1], args[2], args[3]).generateDesugaredLibraryApisDocumetation();
+      return;
+    }
+    System.out.println(
+        "Usage: GenerateLineFiles [--generate-api-docs] "
+            + "<desugar configuration> <desugar implementation> <output directory>");
+    System.exit(1);
   }
 }