| // 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.kotlin; |
| |
| import static com.android.tools.r8.kotlin.Kotlin.addKotlinPrefix; |
| import static com.android.tools.r8.kotlin.KotlinMetadataJvmExtensionUtils.parameterTypesFromJvmMethodSignature; |
| import static com.android.tools.r8.kotlin.KotlinMetadataJvmExtensionUtils.returnTypeFromJvmMethodSignature; |
| import static com.android.tools.r8.utils.DescriptorUtils.descriptorToInternalName; |
| import static com.android.tools.r8.utils.DescriptorUtils.getDescriptorFromKmType; |
| import static kotlinx.metadata.FlagsKt.flagsOf; |
| |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DebugLocalInfo; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.kotlin.KotlinMetadataJvmExtensionUtils.KmConstructorProcessor; |
| import com.android.tools.r8.kotlin.KotlinMetadataJvmExtensionUtils.KmFunctionProcessor; |
| import com.android.tools.r8.kotlin.KotlinMetadataJvmExtensionUtils.KmPropertyProcessor; |
| import com.android.tools.r8.naming.NamingLens; |
| import com.android.tools.r8.shaking.AppInfoWithLiveness; |
| import java.util.List; |
| import kotlinx.metadata.KmConstructor; |
| import kotlinx.metadata.KmFunction; |
| import kotlinx.metadata.KmProperty; |
| import kotlinx.metadata.KmType; |
| import kotlinx.metadata.KmValueParameter; |
| import kotlinx.metadata.jvm.JvmMethodSignature; |
| |
| public class KotlinMetadataSynthesizer { |
| |
| static boolean isExtension(KmFunction kmFunction) { |
| return kmFunction.getReceiverParameterType() != null; |
| } |
| |
| static boolean isExtension(KmProperty kmProperty) { |
| return kmProperty.getReceiverParameterType() != null; |
| } |
| |
| static KmType toKmType(String descriptor) { |
| KmType kmType = new KmType(flagsOf()); |
| kmType.visitClass(descriptorToInternalName(descriptor)); |
| return kmType; |
| } |
| |
| static KmType toRenamedKmType( |
| DexType type, AppView<AppInfoWithLiveness> appView, NamingLens lens) { |
| // E.g., [Ljava/lang/String; -> Lkotlin/Array; |
| if (type.isArrayType()) { |
| return toKmType(addKotlinPrefix("Array;")); |
| } |
| // E.g., void -> Lkotlin/Unit; |
| if (appView.dexItemFactory().kotlin.knownTypeConversion.containsKey(type)) { |
| KmType kmType = new KmType(flagsOf()); |
| DexType convertedType = appView.dexItemFactory().kotlin.knownTypeConversion.get(type); |
| assert convertedType != null; |
| kmType.visitClass(descriptorToInternalName(convertedType.toDescriptorString())); |
| return kmType; |
| } |
| // For library or classpath class, synthesize @Metadata always. |
| // For a program class, make sure it is live. |
| if (!appView.appInfo().isNonProgramTypeOrLiveProgramType(type)) { |
| return null; |
| } |
| DexType renamedType = lens.lookupType(type, appView.dexItemFactory()); |
| // For library or classpath class, we should not have renamed it. |
| DexClass clazz = appView.definitionFor(type); |
| assert clazz == null || clazz.isProgramClass() || renamedType == type |
| : type.toSourceString() + " -> " + renamedType.toSourceString(); |
| // TODO(b/70169921): Mysterious, why attempts to properly set flags bothers kotlinc? |
| // and/or why wiping out flags works for KmType but not KmFunction?! |
| KmType kmType = new KmType(flagsOf()); |
| kmType.visitClass(descriptorToInternalName(renamedType.toDescriptorString())); |
| return kmType; |
| } |
| |
| private static boolean isCompatible(String desc, DexType type) { |
| if (desc == null || type == null) { |
| return false; |
| } |
| return desc.equals(type.toDescriptorString()); |
| } |
| |
| private static boolean isCompatible(KmType kmType, DexType type, AppView<?> appView) { |
| if (kmType == null || type == null) { |
| return false; |
| } |
| String descriptor = null; |
| if (appView.dexItemFactory().kotlin.knownTypeConversion.containsKey(type)) { |
| DexType convertedType = appView.dexItemFactory().kotlin.knownTypeConversion.get(type); |
| descriptor = convertedType.toDescriptorString(); |
| } |
| if (descriptor == null) { |
| descriptor = type.toDescriptorString(); |
| } |
| assert descriptor != null; |
| return descriptor.equals(getDescriptorFromKmType(kmType)); |
| } |
| |
| private static boolean isCompatibleJvmMethodSignature( |
| JvmMethodSignature signature, DexEncodedMethod method) { |
| String methodName = method.method.name.toString(); |
| if (!signature.getName().equals(methodName)) { |
| return false; |
| } |
| if (!isCompatible( |
| returnTypeFromJvmMethodSignature(signature), method.method.proto.returnType)) { |
| return false; |
| } |
| List<String> parameterTypes = parameterTypesFromJvmMethodSignature(signature); |
| if (parameterTypes == null || parameterTypes.size() != method.method.proto.parameters.size()) { |
| return false; |
| } |
| for (int i = 0; i < method.method.proto.parameters.size(); i++) { |
| if (!isCompatible(parameterTypes.get(i), method.method.proto.parameters.values[i])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public static boolean isCompatibleConstructor( |
| KmConstructor constructor, DexEncodedMethod method, AppView<?> appView) { |
| // Note that targets for @JvmName don't include constructor. So, it's not necessary to process |
| // JvmMethodSignature inside JvmConstructorExtension. |
| List<KmValueParameter> parameters = constructor.getValueParameters(); |
| if (method.method.proto.parameters.size() != parameters.size()) { |
| return false; |
| } |
| for (int i = 0; i < method.method.proto.parameters.size(); i++) { |
| KmType kmType = parameters.get(i).getType(); |
| if (!isCompatible(kmType, method.method.proto.parameters.values[i], appView)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public static boolean isCompatibleFunction( |
| KmFunction function, DexEncodedMethod method, AppView<?> appView) { |
| // Check if a custom name is set to avoid name clash. |
| KmFunctionProcessor kmFunctionProcessor = new KmFunctionProcessor(function); |
| JvmMethodSignature jvmMethodSignature = kmFunctionProcessor.signature(); |
| if (jvmMethodSignature != null && isCompatibleJvmMethodSignature(jvmMethodSignature, method)) { |
| return true; |
| } |
| |
| if (!function.getName().equals(method.method.name.toString())) { |
| return false; |
| } |
| if (!isCompatible(function.getReturnType(), method.method.proto.returnType, appView)) { |
| return false; |
| } |
| List<KmValueParameter> parameters = function.getValueParameters(); |
| if (method.method.proto.parameters.size() != parameters.size()) { |
| return false; |
| } |
| for (int i = 0; i < method.method.proto.parameters.size(); i++) { |
| KmType kmType = parameters.get(i).getType(); |
| if (!isCompatible(kmType, method.method.proto.parameters.values[i], appView)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public static boolean isCompatibleExtension( |
| KmFunction extension, DexEncodedMethod method, AppView<?> appView) { |
| // Check if a custom name is set to avoid name clash. |
| KmFunctionProcessor kmFunctionProcessor = new KmFunctionProcessor(extension); |
| JvmMethodSignature jvmMethodSignature = kmFunctionProcessor.signature(); |
| if (jvmMethodSignature != null && isCompatibleJvmMethodSignature(jvmMethodSignature, method)) { |
| return true; |
| } |
| |
| if (!extension.getName().equals(method.method.name.toString())) { |
| return false; |
| } |
| if (!isCompatible(extension.getReturnType(), method.method.proto.returnType, appView)) { |
| return false; |
| } |
| List<KmValueParameter> parameters = extension.getValueParameters(); |
| if (method.method.proto.parameters.size() != parameters.size() + 1) { |
| return false; |
| } |
| assert method.method.proto.parameters.size() > 0; |
| assert extension.getReceiverParameterType() != null; |
| if (!isCompatible( |
| extension.getReceiverParameterType(), method.method.proto.parameters.values[0], appView)) { |
| return false; |
| } |
| for (int i = 1; i < method.method.proto.parameters.size(); i++) { |
| KmType kmType = parameters.get(i - 1).getType(); |
| if (!isCompatible(kmType, method.method.proto.parameters.values[i], appView)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public static boolean isCompatibleProperty( |
| KmProperty kmProperty, DexEncodedMethod method, AppView<?> appView) { |
| KmPropertyProcessor kmPropertyProcessor = new KmPropertyProcessor(kmProperty); |
| // Check if a custom getter is defined via @get:JvmName("myGetter"). |
| JvmMethodSignature getterSignature = kmPropertyProcessor.getterSignature(); |
| if (getterSignature != null && isCompatibleJvmMethodSignature(getterSignature, method)) { |
| return true; |
| } |
| // Check if a custom setter is defined via @set:JvmName("mySetter"). |
| JvmMethodSignature setterSignature = kmPropertyProcessor.setterSignature(); |
| if (setterSignature != null && isCompatibleJvmMethodSignature(setterSignature, method)) { |
| return true; |
| } |
| |
| // E.g., property `prop: T` is mapped to `getProp()T`, `setProp(T)V`, `prop$annotations()V`. |
| // For boolean property, though, getter is mapped to `isProp()Z`. |
| // TODO(b/70169921): Avoid decoding. |
| String methodName = method.method.name.toString(); |
| if (!methodName.startsWith("is") |
| && !methodName.startsWith("get") |
| && !methodName.startsWith("set") |
| && !methodName.endsWith("$annotations")) { |
| return false; |
| } |
| |
| String propertyName = kmProperty.getName(); |
| assert propertyName.length() > 0; |
| String annotations = propertyName + "$annotations"; |
| if (methodName.equals(annotations)) { |
| return true; |
| } |
| String capitalized = |
| Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); |
| String returnTypeDescriptor = getDescriptorFromKmType(kmProperty.returnType); |
| String getterPrefix = |
| returnTypeDescriptor != null && returnTypeDescriptor.endsWith("Boolean;") ? "is" : "get"; |
| String getter = getterPrefix + capitalized; |
| if (methodName.equals(getter) |
| && method.method.proto.parameters.size() == 0 |
| && isCompatible(kmProperty.returnType, method.method.proto.returnType, appView)) { |
| return true; |
| } |
| String setter = "set" + capitalized; |
| if (methodName.equals(setter) |
| && method.method.proto.returnType.isVoidType() |
| && method.method.proto.parameters.size() == 1 |
| && isCompatible(kmProperty.returnType, method.method.proto.parameters.values[0], appView)) { |
| return true; |
| } |
| return false; |
| } |
| |
| static KmConstructor toRenamedKmConstructor( |
| DexEncodedMethod method, |
| KmConstructor original, |
| AppView<AppInfoWithLiveness> appView, |
| NamingLens lens) { |
| // Make sure it is an instance initializer and live. |
| if (!method.isInstanceInitializer() |
| || !appView.appInfo().liveMethods.contains(method.method)) { |
| return null; |
| } |
| // TODO(b/70169921): {@link KmConstructor.extensions} is private, i.e., no way to alter! |
| // Thus, we rely on original metadata for now. |
| KmConstructorProcessor kmConstructorProcessor = new KmConstructorProcessor(original); |
| JvmMethodSignature jvmMethodSignature = kmConstructorProcessor.signature(); |
| KmConstructor kmConstructor = |
| jvmMethodSignature != null |
| ? original |
| // TODO(b/70169921): Consult kotlinx.metadata.Flag.Constructor to set IS_PRIMARY. |
| : new KmConstructor(method.accessFlags.getAsKotlinFlags()); |
| List<KmValueParameter> parameters = kmConstructor.getValueParameters(); |
| parameters.clear(); |
| if (!populateKmValueParameters(parameters, method, appView, lens, false)) { |
| return null; |
| } |
| return kmConstructor; |
| } |
| |
| static KmFunction toRenamedKmFunction( |
| DexEncodedMethod method, |
| KmFunction original, |
| AppView<AppInfoWithLiveness> appView, |
| NamingLens lens) { |
| return toRenamedKmFunctionHelper(method, original, appView, lens, false); |
| } |
| |
| static KmFunction toRenamedKmFunctionAsExtension( |
| DexEncodedMethod method, |
| KmFunction original, |
| AppView<AppInfoWithLiveness> appView, |
| NamingLens lens) { |
| return toRenamedKmFunctionHelper(method, original, appView, lens, true); |
| } |
| |
| private static KmFunction toRenamedKmFunctionHelper( |
| DexEncodedMethod method, |
| KmFunction original, |
| AppView<AppInfoWithLiveness> appView, |
| NamingLens lens, |
| boolean isExtension) { |
| // For library overrides, synthesize @Metadata always. |
| // For regular methods, make sure it is live. |
| if (!method.isLibraryMethodOverride().isTrue() |
| && !appView.appInfo().liveMethods.contains(method.method)) { |
| return null; |
| } |
| DexMethod renamedMethod = lens.lookupMethod(method.method, appView.dexItemFactory()); |
| // For a library method override, we should not have renamed it. |
| assert !method.isLibraryMethodOverride().isTrue() || renamedMethod == method.method |
| : method.toSourceString() + " -> " + renamedMethod.toSourceString(); |
| // TODO(b/70169921): {@link KmFunction.extensions} is private, i.e., no way to alter! |
| // Thus, we rely on original metadata for now. |
| assert !isExtension || original != null; |
| KmFunction kmFunction = |
| isExtension |
| ? original |
| // TODO(b/70169921): Consult kotlinx.metadata.Flag.Function for kind (e.g., suspend). |
| : new KmFunction(method.accessFlags.getAsKotlinFlags(), renamedMethod.name.toString()); |
| KmType kmReturnType = toRenamedKmType(method.method.proto.returnType, appView, lens); |
| if (kmReturnType == null) { |
| return null; |
| } |
| kmFunction.setReturnType(kmReturnType); |
| if (isExtension) { |
| assert method.method.proto.parameters.values.length > 0; |
| KmType kmReceiverType = |
| toRenamedKmType(method.method.proto.parameters.values[0], appView, lens); |
| if (kmReceiverType == null) { |
| return null; |
| } |
| kmFunction.setReceiverParameterType(kmReceiverType); |
| } |
| List<KmValueParameter> parameters = kmFunction.getValueParameters(); |
| parameters.clear(); |
| if (!populateKmValueParameters(parameters, method, appView, lens, isExtension)) { |
| return null; |
| } |
| return kmFunction; |
| } |
| |
| private static boolean populateKmValueParameters( |
| List<KmValueParameter> parameters, |
| DexEncodedMethod method, |
| AppView<AppInfoWithLiveness> appView, |
| NamingLens lens, |
| boolean isExtension) { |
| for (int i = isExtension ? 1 : 0; i < method.method.proto.parameters.values.length; i++) { |
| DexType paramType = method.method.proto.parameters.values[i]; |
| DebugLocalInfo debugLocalInfo = method.getParameterInfo().get(i); |
| String parameterName = debugLocalInfo != null ? debugLocalInfo.name.toString() : ("p" + i); |
| // TODO(b/70169921): Consult kotlinx.metadata.Flag.ValueParameter. |
| KmValueParameter kmValueParameter = new KmValueParameter(flagsOf(), parameterName); |
| KmType kmParamType = toRenamedKmType(paramType, appView, lens); |
| if (kmParamType == null) { |
| return false; |
| } |
| kmValueParameter.setType(kmParamType); |
| parameters.add(kmValueParameter); |
| } |
| return true; |
| } |
| } |