Extend @Metadata compatibility checks to JVM extensions and properties.

Test: tools/test.py *metadata*
Test: tools/run_on_as_app.py --app rover-android --shrinker r8
Test: tools/run_on_as_app.py --app tivi --shrinker r8
Test: tools/run_on_as_app.py --app Simple-Calendar --shrinker r8

Bug: 70169921
Change-Id: Icc0752ed01e0833bd0bda31daedcb18ba42f4fe5
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 77545740..815e721 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -250,7 +250,7 @@
       if (function != null) {
         // Found a compatible function that is likely asked to keep.
         builder.add(method);
-      } else if (!method.isKotlinProperty(properties)) {
+      } else if (!method.isKotlinProperty(properties, appView)) {
         // This could be a newly merged method that is not part of properties.
         builder.add(method);
       }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index a928529..39977cc 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -398,35 +398,20 @@
     return null;
   }
 
-  // E.g., property `prop: T` is mapped to `getProp()T`, `setProp(T)V`, `prop$annotations()V`.
-  // TODO(b/70169921): Handle different name patterns via @JvmName.
-  boolean isKotlinProperty(List<KmProperty> properties) {
-    // TODO(b/70169921): Avoid decoding.
-    String methodName = method.name.toString();
-    if (!methodName.startsWith("get")
-        && !methodName.startsWith("set")
-        && !methodName.endsWith("$annotations")) {
-      return false;
+  boolean isKotlinProperty(List<KmProperty> properties, AppView<?> appView) {
+    return findCompatibleKotlinProperty(properties, appView) != null;
+  }
+
+  KmProperty findCompatibleKotlinProperty(List<KmProperty> properties, AppView<?> appView) {
+    if (isStaticMember()) {
+      return null;
     }
     for (KmProperty property : properties) {
-      String propertyName = property.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 getter = "get" + capitalized;
-      if (methodName.equals(getter)) {
-        return true;
-      }
-      String setter = "set" + capitalized;
-      if (methodName.equals(setter)) {
-        return true;
+      if (KotlinMetadataSynthesizer.isCompatibleProperty(property, this, appView)) {
+        return property;
       }
     }
-    return false;
+    return null;
   }
 
   public boolean isOnlyInlinedIntoNestMembers() {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataJvmExtensionUtils.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataJvmExtensionUtils.java
new file mode 100644
index 0000000..66208ad
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataJvmExtensionUtils.java
@@ -0,0 +1,148 @@
+// 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.kotlin;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.List;
+import kotlinx.metadata.KmExtensionType;
+import kotlinx.metadata.KmFunction;
+import kotlinx.metadata.KmFunctionExtensionVisitor;
+import kotlinx.metadata.KmFunctionVisitor;
+import kotlinx.metadata.KmProperty;
+import kotlinx.metadata.KmPropertyExtensionVisitor;
+import kotlinx.metadata.KmPropertyVisitor;
+import kotlinx.metadata.jvm.JvmFieldSignature;
+import kotlinx.metadata.jvm.JvmFunctionExtensionVisitor;
+import kotlinx.metadata.jvm.JvmMethodSignature;
+import kotlinx.metadata.jvm.JvmPropertyExtensionVisitor;
+
+class KotlinMetadataJvmExtensionUtils {
+
+  private static boolean isValidJvmMethodSignature(String desc) {
+    return desc != null
+        && !desc.isEmpty()
+        && desc.charAt(0) == '('
+        && desc.lastIndexOf('(') == 0
+        && desc.indexOf(')') != -1
+        && desc.indexOf(')') == desc.lastIndexOf(')')
+        && desc.lastIndexOf(')') < desc.length();
+  }
+
+  /**
+   * Extract return type from {@link JvmMethodSignature}.
+   *
+   * Example of JVM signature is: `JvmMethodSignature("getX", "()Ljava/lang/Object;").`
+   * In this case, the return type is "Ljava/lang/Object;".
+   */
+  static String returnTypeFromJvmMethodSignature(JvmMethodSignature signature) {
+    if (signature == null) {
+      return null;
+    }
+    String desc = signature.getDesc();
+    if (!isValidJvmMethodSignature(desc)) {
+      return null;
+    }
+    int index = desc.lastIndexOf(')');
+    assert desc.charAt(0) == '(' && 0 < index && index < desc.length() : signature.asString();
+    return desc.substring(index + 1);
+  }
+
+  /**
+   * Extract parameters from {@link JvmMethodSignature}.
+   *
+   * Example of JVM signature is: `JvmMethodSignature("setX", "(Ljava/lang/Object;)V").`
+   * In this case, the parameter is the list with "Ljava/lang/Object;" as the first element.
+   */
+  static List<String> parameterTypesFromJvmMethodSignature(JvmMethodSignature signature) {
+    if (signature == null) {
+      return null;
+    }
+    String desc = signature.getDesc();
+    if (!isValidJvmMethodSignature(desc)) {
+      return null;
+    }
+    int index = desc.lastIndexOf(')');
+    assert desc.charAt(0) == '(' && 0 < index && index < desc.length() : signature.asString();
+    String params = desc.substring(1, index);
+    if (params.isEmpty()) {
+      return ImmutableList.of();
+    } else {
+      return Arrays.asList(params.split(","));
+    }
+  }
+
+  static class KmFunctionProcessor {
+    // Custom name via @JvmName("..."). Otherwise, null.
+    private JvmMethodSignature signature = null;
+
+    KmFunctionProcessor(KmFunction kmFunction) {
+      kmFunction.accept(new KmFunctionVisitor() {
+        @Override
+        public KmFunctionExtensionVisitor visitExtensions(KmExtensionType type) {
+          if (type != JvmFunctionExtensionVisitor.TYPE) {
+            return null;
+          }
+          return new JvmFunctionExtensionVisitor() {
+            @Override
+            public void visit(JvmMethodSignature desc) {
+              assert signature == null : signature.asString();
+              signature = desc;
+            }
+          };
+        }
+      });
+    }
+
+    JvmMethodSignature signature() {
+      return signature;
+    }
+  }
+
+  static class KmPropertyProcessor {
+    private JvmFieldSignature fieldSignature = null;
+    // Custom getter via @get:JvmName("..."). Otherwise, null.
+    private JvmMethodSignature getterSignature = null;
+    // Custom getter via @set:JvmName("..."). Otherwise, null.
+    private JvmMethodSignature setterSignature = null;
+
+    KmPropertyProcessor(KmProperty kmProperty) {
+      kmProperty.accept(new KmPropertyVisitor() {
+        @Override
+        public KmPropertyExtensionVisitor visitExtensions(KmExtensionType type) {
+          if (type != JvmPropertyExtensionVisitor.TYPE) {
+            return null;
+          }
+          return new JvmPropertyExtensionVisitor() {
+            @Override
+            public void visit(
+                int flags,
+                JvmFieldSignature fieldDesc,
+                JvmMethodSignature getterDesc,
+                JvmMethodSignature setterDesc) {
+              assert fieldSignature == null : fieldSignature.asString();
+              fieldSignature = fieldDesc;
+              assert getterSignature == null : getterSignature.asString();
+              getterSignature = getterDesc;
+              assert setterSignature == null : setterSignature.asString();
+              setterSignature = setterDesc;
+            }
+          };
+        }
+      });
+    }
+
+    JvmFieldSignature fieldSignature() {
+      return fieldSignature;
+    }
+
+    JvmMethodSignature getterSignature() {
+      return getterSignature;
+    }
+
+    JvmMethodSignature setterSignature() {
+      return setterSignature;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java
index ff9ad15..80069a2 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java
@@ -4,6 +4,8 @@
 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;
@@ -14,14 +16,18 @@
 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.KmFunctionProcessor;
+import com.android.tools.r8.kotlin.KotlinMetadataJvmExtensionUtils.KmPropertyProcessor;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Box;
 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 {
 
@@ -69,6 +75,13 @@
     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;
@@ -85,8 +98,32 @@
     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;
@@ -102,6 +139,13 @@
 
   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;
     }
@@ -121,9 +165,15 @@
     return true;
   }
 
-  // TODO(b/70169921): Handling JVM extensions as well.
   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;
     }
@@ -149,6 +199,58 @@
     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,