| // Copyright (c) 2019, 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; | |
| import com.android.tools.r8.cf.CfVersion; | |
| import com.android.tools.r8.cf.code.CfConstNull; | |
| import com.android.tools.r8.cf.code.CfInstruction; | |
| import com.android.tools.r8.cf.code.CfThrow; | |
| import com.android.tools.r8.dex.ApplicationReader; | |
| import com.android.tools.r8.dex.Marker.Tool; | |
| 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.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; | |
| import com.android.tools.r8.ir.desugar.DesugaredLibraryConfigurationParser; | |
| import com.android.tools.r8.jar.CfApplicationWriter; | |
| import com.android.tools.r8.naming.NamingLens; | |
| import com.android.tools.r8.origin.Origin; | |
| import com.android.tools.r8.utils.AndroidApiLevel; | |
| import com.android.tools.r8.utils.AndroidApp; | |
| import com.android.tools.r8.utils.DescriptorUtils; | |
| import com.android.tools.r8.utils.FileUtils; | |
| import com.android.tools.r8.utils.InternalOptions; | |
| import com.android.tools.r8.utils.Reporter; | |
| import com.android.tools.r8.utils.StringUtils; | |
| 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; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.Comparator; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.function.BiPredicate; | |
| import java.util.function.Predicate; | |
| import java.util.stream.StreamSupport; | |
| public class GenerateLintFiles { | |
| private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar"; | |
| private final DexItemFactory factory = new DexItemFactory(); | |
| private final Reporter reporter = new Reporter(); | |
| private final InternalOptions options = new InternalOptions(factory, reporter); | |
| private final DesugaredLibraryConfiguration desugaredLibraryConfiguration; | |
| private final Path desugaredLibraryImplementation; | |
| private final Path outputDirectory; | |
| private final Set<DexMethod> parallelMethods = Sets.newIdentityHashSet(); | |
| public GenerateLintFiles( | |
| String desugarConfigurationPath, String desugarImplementationPath, String outputDirectory) | |
| throws Exception { | |
| this.desugaredLibraryConfiguration = | |
| readDesugaredLibraryConfiguration(desugarConfigurationPath); | |
| this.desugaredLibraryImplementation = Paths.get(desugarImplementationPath); | |
| this.outputDirectory = Paths.get(outputDirectory); | |
| if (!Files.isDirectory(this.outputDirectory)) { | |
| throw new Exception("Output directory " + outputDirectory + " is not a directory"); | |
| } | |
| DexType streamType = factory.createType(factory.createString("Ljava/util/stream/Stream;")); | |
| DexMethod parallelMethod = | |
| factory.createMethod( | |
| factory.collectionType, | |
| factory.createProto(streamType), | |
| factory.createString("parallelStream")); | |
| parallelMethods.add(parallelMethod); | |
| DexType baseStreamType = | |
| factory.createType(factory.createString("Ljava/util/stream/BaseStream;")); | |
| for (String typePrefix : new String[] {"Base", "Double", "Int", "Long"}) { | |
| streamType = | |
| factory.createType(factory.createString("Ljava/util/stream/" + typePrefix + "Stream;")); | |
| parallelMethod = | |
| factory.createMethod( | |
| streamType, factory.createProto(streamType), factory.createString("parallel")); | |
| parallelMethods.add(parallelMethod); | |
| // Also filter out the generated bridges for the covariant return type. | |
| parallelMethod = | |
| factory.createMethod( | |
| streamType, factory.createProto(baseStreamType), factory.createString("parallel")); | |
| parallelMethods.add(parallelMethod); | |
| } | |
| } | |
| private static Path getAndroidJarPath(AndroidApiLevel apiLevel) { | |
| String jar = String.format(ANDROID_JAR_PATTERN, apiLevel.getLevel()); | |
| return Paths.get(jar); | |
| } | |
| private DesugaredLibraryConfiguration readDesugaredLibraryConfiguration( | |
| String desugarConfigurationPath) { | |
| return new DesugaredLibraryConfigurationParser( | |
| factory, reporter, false, AndroidApiLevel.B.getLevel()) | |
| .parse(StringResource.fromFile(Paths.get(desugarConfigurationPath))); | |
| } | |
| private CfCode buildEmptyThrowingCfCode(DexMethod method) { | |
| CfInstruction insn[] = {new CfConstNull(), new CfThrow()}; | |
| return new CfCode( | |
| method.holder, | |
| 1, | |
| method.proto.parameters.size() + 1, | |
| Arrays.asList(insn), | |
| Collections.emptyList(), | |
| Collections.emptyList()); | |
| } | |
| private void addMethodsToHeaderJar( | |
| DexApplication.Builder builder, DexClass clazz, List<DexEncodedMethod> methods) { | |
| if (methods.size() == 0) { | |
| return; | |
| } | |
| List<DexEncodedMethod> directMethods = new ArrayList<>(); | |
| List<DexEncodedMethod> virtualMethods = new ArrayList<>(); | |
| for (DexEncodedMethod method : methods) { | |
| assert method.getHolderType() == clazz.type; | |
| CfCode code = null; | |
| if (!method.accessFlags.isAbstract() /*&& !method.accessFlags.isNative()*/) { | |
| code = buildEmptyThrowingCfCode(method.getReference()); | |
| } | |
| DexEncodedMethod throwingMethod = | |
| new DexEncodedMethod( | |
| method.getReference(), | |
| method.accessFlags, | |
| MethodTypeSignature.noSignature(), | |
| DexAnnotationSet.empty(), | |
| ParameterAnnotationsList.empty(), | |
| code, | |
| false, | |
| CfVersion.V1_6, | |
| false); | |
| if (method.isStatic() || method.isDirectMethod()) { | |
| directMethods.add(throwingMethod); | |
| } else { | |
| virtualMethods.add(throwingMethod); | |
| } | |
| } | |
| DexEncodedMethod[] directMethodsArray = new DexEncodedMethod[directMethods.size()]; | |
| DexEncodedMethod[] virtualMethodsArray = new DexEncodedMethod[virtualMethods.size()]; | |
| directMethods.toArray(directMethodsArray); | |
| virtualMethods.toArray(virtualMethodsArray); | |
| assert !options.encodeChecksums; | |
| ChecksumSupplier checksumSupplier = DexProgramClass::invalidChecksumRequest; | |
| DexProgramClass programClass = | |
| new DexProgramClass( | |
| clazz.type, | |
| null, | |
| Origin.unknown(), | |
| clazz.accessFlags, | |
| clazz.superType, | |
| clazz.interfaces, | |
| null, | |
| null, | |
| Collections.emptyList(), | |
| null, | |
| Collections.emptyList(), | |
| ClassSignature.noSignature(), | |
| DexAnnotationSet.empty(), | |
| DexEncodedField.EMPTY_ARRAY, | |
| DexEncodedField.EMPTY_ARRAY, | |
| directMethodsArray, | |
| virtualMethodsArray, | |
| false, | |
| checksumSupplier); | |
| builder.addProgramClass(programClass); | |
| } | |
| public static class SupportedMethods { | |
| public final Set<DexClass> classesWithAllMethodsSupported; | |
| public final Map<DexClass, List<DexEncodedMethod>> supportedMethods; | |
| public SupportedMethods( | |
| Set<DexClass> classesWithAllMethodsSupported, | |
| Map<DexClass, List<DexEncodedMethod>> supportedMethods) { | |
| this.classesWithAllMethodsSupported = classesWithAllMethodsSupported; | |
| this.supportedMethods = supportedMethods; | |
| } | |
| } | |
| 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().addProgramFiles(getAndroidJarPath(compilationApiLevel)).build(); | |
| DirectMappedDexApplication dexApplication = | |
| new ApplicationReader(library, options, Timing.empty()).read().toDirect(); | |
| AndroidApp implementation = | |
| AndroidApp.builder().addProgramFiles(desugaredLibraryImplementation).build(); | |
| DirectMappedDexApplication implementationApplication = | |
| new ApplicationReader(implementation, options, Timing.empty()).read().toDirect(); | |
| // 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 (DexProgramClass clazz : dexApplication.classes()) { | |
| String className = clazz.toSourceString(); | |
| // All the methods with the rewritten prefix are supported. | |
| for (String prefix : desugaredLibraryConfiguration.getRewritePrefix().keySet()) { | |
| if (clazz.accessFlags.isPublic() && className.startsWith(prefix)) { | |
| DexProgramClass implementationClass = | |
| implementationApplication.programDefinitionFor(clazz.getType()); | |
| if (implementationClass == null) { | |
| throw new Exception("Implementation class not found for " + clazz.toSourceString()); | |
| } | |
| boolean allMethodsAddad = true; | |
| for (DexEncodedMethod method : clazz.methods()) { | |
| if (!method.isPublic()) { | |
| continue; | |
| } | |
| ProgramMethod implementationMethod = | |
| implementationClass.lookupProgramMethod(method.getReference()); | |
| // Don't include methods which are not implemented by the desugared library. | |
| if (supported.test(method) && implementationMethod != null) { | |
| supportedMethods.computeIfAbsent(clazz, k -> new ArrayList<>()).add(method); | |
| } else { | |
| allMethodsAddad = false; | |
| } | |
| } | |
| if (allMethodsAddad) { | |
| classesWithAllMethodsSupported.add(clazz); | |
| } | |
| } | |
| } | |
| // All retargeted methods are supported. | |
| for (DexEncodedMethod method : clazz.methods()) { | |
| if (desugaredLibraryConfiguration | |
| .getRetargetCoreLibMember() | |
| .keySet() | |
| .contains(method.getReference().name)) { | |
| if (desugaredLibraryConfiguration | |
| .getRetargetCoreLibMember() | |
| .get(method.getReference().name) | |
| .containsKey(clazz.type)) { | |
| if (supported.test(method)) { | |
| supportedMethods.computeIfAbsent(clazz, k -> new ArrayList<>()).add(method); | |
| } | |
| } | |
| } | |
| } | |
| // All emulated interfaces static and default methods are supported. | |
| if (desugaredLibraryConfiguration.getEmulateLibraryInterface().containsKey(clazz.type)) { | |
| assert clazz.isInterface(); | |
| for (DexEncodedMethod method : clazz.methods()) { | |
| if (!method.isDefaultMethod() && !method.isStatic()) { | |
| continue; | |
| } | |
| if (supported.test(method)) { | |
| supportedMethods.computeIfAbsent(clazz, k -> new ArrayList<>()).add(method); | |
| } | |
| } | |
| } | |
| } | |
| return new SupportedMethods(classesWithAllMethodsSupported, supportedMethods); | |
| } | |
| private String lintBaseFileName( | |
| AndroidApiLevel compilationApiLevel, AndroidApiLevel minApiLevel) { | |
| return "desugared_apis_" + compilationApiLevel.getLevel() + "_" + minApiLevel.getLevel(); | |
| } | |
| private Path lintFile( | |
| AndroidApiLevel compilationApiLevel, AndroidApiLevel minApiLevel, String extension) | |
| throws Exception { | |
| Path directory = outputDirectory.resolve("compile_api_level_" + compilationApiLevel.getLevel()); | |
| Files.createDirectories(directory); | |
| return Paths.get( | |
| directory | |
| + File.separator | |
| + lintBaseFileName(compilationApiLevel, minApiLevel) | |
| + extension); | |
| } | |
| private void writeLintFiles( | |
| AndroidApiLevel compilationApiLevel, | |
| AndroidApiLevel minApiLevel, | |
| SupportedMethods supportedMethods) | |
| throws Exception { | |
| // Build a plain text file with the desugared APIs. | |
| List<String> desugaredApisSignatures = new ArrayList<>(); | |
| LazyLoadedDexApplication.Builder builder = DexApplication.builder(options, Timing.empty()); | |
| supportedMethods.supportedMethods.forEach( | |
| (clazz, methods) -> { | |
| String classBinaryName = | |
| DescriptorUtils.getClassBinaryNameFromDescriptor(clazz.type.descriptor.toString()); | |
| if (!supportedMethods.classesWithAllMethodsSupported.contains(clazz)) { | |
| for (DexEncodedMethod method : methods) { | |
| if (method.isInstanceInitializer() || method.isClassInitializer()) { | |
| // No new constructors are added. | |
| continue; | |
| } | |
| desugaredApisSignatures.add( | |
| classBinaryName | |
| + '#' | |
| + method.getReference().name | |
| + method.getReference().proto.toDescriptorString()); | |
| } | |
| } else { | |
| desugaredApisSignatures.add(classBinaryName); | |
| } | |
| addMethodsToHeaderJar(builder, clazz, methods); | |
| }); | |
| // Write a plain text file with the desugared APIs. | |
| desugaredApisSignatures.sort(Comparator.naturalOrder()); | |
| FileUtils.writeTextFile( | |
| lintFile(compilationApiLevel, minApiLevel, ".txt"), desugaredApisSignatures); | |
| // Write a header jar with the desugared APIs. | |
| AppView<?> appView = AppView.createForD8(AppInfo.createInitialAppInfo(builder.build())); | |
| CfApplicationWriter writer = | |
| new CfApplicationWriter( | |
| appView, | |
| options.getMarker(Tool.L8), | |
| GraphLens.getIdentityLens(), | |
| NamingLens.getIdentityLens(), | |
| null); | |
| ClassFileConsumer consumer = | |
| new ClassFileConsumer.ArchiveConsumer( | |
| lintFile(compilationApiLevel, minApiLevel, FileUtils.JAR_EXTENSION)); | |
| writer.write(consumer); | |
| consumer.finished(options.reporter); | |
| } | |
| private void generateLintFiles( | |
| AndroidApiLevel compilationApiLevel, | |
| Predicate<AndroidApiLevel> generateForThisMinApiLevel, | |
| BiPredicate<AndroidApiLevel, DexEncodedMethod> supportedForMinApiLevel) | |
| throws Exception { | |
| System.out.print(" - generating for min API:"); | |
| for (AndroidApiLevel minApiLevel : AndroidApiLevel.values()) { | |
| if (!generateForThisMinApiLevel.test(minApiLevel)) { | |
| continue; | |
| } | |
| System.out.print(" " + minApiLevel); | |
| SupportedMethods supportedMethods = | |
| collectSupportedMethods( | |
| compilationApiLevel, (method -> supportedForMinApiLevel.test(minApiLevel, method))); | |
| writeLintFiles(compilationApiLevel, minApiLevel, supportedMethods); | |
| } | |
| System.out.println(); | |
| } | |
| private void run() throws Exception { | |
| // Run over all the API levels that the desugared library can be compiled with. | |
| for (int apiLevel = AndroidApiLevel.LATEST.getLevel(); | |
| apiLevel >= desugaredLibraryConfiguration.getRequiredCompilationApiLevel().getLevel(); | |
| apiLevel--) { | |
| System.out.println("Generating lint files for compile API " + apiLevel); | |
| run(apiLevel); | |
| } | |
| } | |
| public void run(int apiLevel) throws Exception { | |
| generateLintFiles( | |
| AndroidApiLevel.getAndroidApiLevel(apiLevel), | |
| minApiLevel -> minApiLevel == AndroidApiLevel.L || minApiLevel == AndroidApiLevel.B, | |
| (minApiLevel, method) -> { | |
| assert minApiLevel == AndroidApiLevel.L || minApiLevel == AndroidApiLevel.B; | |
| if (minApiLevel == AndroidApiLevel.L) { | |
| return true; | |
| } | |
| assert minApiLevel == AndroidApiLevel.B; | |
| return !parallelMethods.contains(method.getReference()); | |
| }); | |
| } | |
| private static class StringBuilderWithIndent { | |
| String NL = System.lineSeparator(); | |
| StringBuilder builder = new StringBuilder(); | |
| String indent = ""; | |
| StringBuilderWithIndent() {} | |
| StringBuilderWithIndent indent(String indent) { | |
| this.indent = indent; | |
| return this; | |
| } | |
| StringBuilderWithIndent appendLine(String line) { | |
| builder.append(indent); | |
| builder.append(line); | |
| builder.append(NL); | |
| return this; | |
| } | |
| StringBuilderWithIndent emptyLine() { | |
| builder.append(NL); | |
| return this; | |
| } | |
| @Override | |
| 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 typeName, String packageName) { | |
| 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(typeName, packageName); | |
| if (result == null) { | |
| result = typeInPackage(typeName, "java.lang"); | |
| } | |
| 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.getReference().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; | |
| } | |
| @Override | |
| public HTMLSourceBuilder self() { | |
| return this; | |
| } | |
| @Override | |
| 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.getReference().type) | |
| + " " | |
| + field.getReference().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.getReference().proto.returnType) | |
| + " " | |
| + method.getReference().name | |
| + arguments(method)); | |
| if (parallelMethods.contains(method.getReference())) { | |
| parallelM.add(method.getReference().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; | |
| } | |
| throw new RuntimeException( | |
| StringUtils.joinLines( | |
| "Invalid invocation.", | |
| "Usage: GenerateLineFiles [--generate-api-docs] " | |
| + "<desugar configuration> <desugar implementation> <output directory>")); | |
| } | |
| } |