Enable merging of partial signature files am: 23df0a11a2 am: e2e98838ca am: f3d7c9144d am: 9d95426825

Original change: https://android-review.googlesource.com/c/platform/tools/metalava/+/2380672

Change-Id: I48f638e6edf9d4ef0414d7de05b8881a4ec535d8
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt b/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
index 8fe3dde..20aaa97 100644
--- a/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
@@ -276,9 +276,6 @@
         assertIdent(tokenizer, token)
         val name: String = token
         qualifiedName = qualifiedName(pkg.name(), name)
-        if (api.findClass(qualifiedName) != null) {
-            throw ApiParseException("Duplicate class found: $qualifiedName", tokenizer)
-        }
         val typeInfo = api.obtainTypeFromString(qualifiedName)
         // Simple type info excludes the package name (but includes enclosing class names)
         var rawName = name
@@ -287,11 +284,22 @@
             rawName = rawName.substring(0, variableIndex)
         }
         token = tokenizer.requireToken()
-        cl = TextClassItem(
+        val cls = TextClassItem(
             api, tokenizer.pos(), modifiers, isInterface, isEnum, isAnnotation,
             typeInfo.toErasedTypeString(null), typeInfo.qualifiedTypeName(),
             rawName, annotations
         )
+        cl = when (val foundClass = api.findClass(qualifiedName)) {
+            null -> cls
+            else -> {
+                if (!foundClass.isCompatible(cls)) {
+                    throw ApiParseException("Incompatible $foundClass definitions")
+                } else {
+                    foundClass
+                }
+            }
+        }
+
         cl.setContainingPackage(pkg)
         cl.setTypeInfo(typeInfo)
         cl.deprecated = modifiers.isDeprecated()
@@ -556,7 +564,9 @@
         if (";" != token) {
             throw ApiParseException("expected ; found $token", tokenizer)
         }
-        cl.addMethod(method)
+        if (!cl.methods().contains(method)) {
+            cl.addMethod(method)
+        }
     }
 
     private fun mergeAnnotations(
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
index c0a133f..98a3257 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
@@ -225,6 +225,22 @@
         innerClasses.add(cls)
     }
 
+    fun isCompatible(cls: TextClassItem): Boolean {
+        if (this === cls) {
+            return true
+        }
+        if (fullName != cls.fullName) {
+            return false
+        }
+
+        return modifiers.toString() == cls.modifiers.toString() &&
+            isInterface == cls.isInterface &&
+            isEnum == cls.isEnum &&
+            isAnnotation == cls.isAnnotation &&
+            superClass == cls.superClass &&
+            allInterfaces().toSet() == cls.allInterfaces().toSet()
+    }
+
     override fun filteredSuperClassType(predicate: Predicate<Item>): TypeItem? {
         // No filtering in signature files: we assume signature APIs
         // have already been filtered and all items should match.
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt b/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
index 815d688..94ed894 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
@@ -104,7 +104,9 @@
         if (!mClassToInterface.containsKey(classInfo)) {
             mClassToInterface[classInfo] = ArrayList()
         }
-        mClassToInterface[classInfo]?.add(iface)
+        mClassToInterface[classInfo]?.let {
+            if (!it.contains(iface)) it.add(iface)
+        }
     }
 
     fun implementsInterface(classInfo: TextClassItem, iface: String): Boolean {
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
index d3bfc4d..7b126c3 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
@@ -31,10 +31,17 @@
 
     private val classes = ArrayList<TextClassItem>(100)
 
+    private val classesNames = HashSet<String>(100)
+
     fun name() = name
 
     fun addClass(classInfo: TextClassItem) {
+        val classFullName = classInfo.fullName()
+        if (classFullName in classesNames) {
+            return
+        }
         classes.add(classInfo)
+        classesNames.add(classFullName)
     }
 
     internal fun pruneClassList() {
diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
index e3c571c..23944c6 100644
--- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
@@ -3802,7 +3802,7 @@
     }
 
     @Test
-    fun `Test cannot merging API signature files with duplicate class`() {
+    fun `Test can merge API signature files with duplicate class`() {
         val source1 = """
             package Test.pkg {
               public final class Class1 {
@@ -3817,9 +3817,38 @@
               }
             }
                     """
+        val expected = """
+            package Test.pkg {
+              public final class Class1 {
+                method public void method1();
+              }
+            }
+                    """
         check(
             signatureSources = arrayOf(source1, source2),
-            expectedFail = "Aborting: Unable to parse signature file: TESTROOT/project/load-api2.txt:2: Duplicate class found: Test.pkg.Class1"
+            api = expected
+        )
+    }
+
+    @Test
+    fun `Test cannot merge API signature files with incompatible class definitions`() {
+        val source1 = """
+            package Test.pkg {
+              public class Class1 {
+                method public void method2();
+              }
+            }
+                    """
+        val source2 = """
+            package Test.pkg {
+              public final class Class1 {
+                method public void method1();
+              }
+            }
+                    """
+        check(
+            signatureSources = arrayOf(source1, source2),
+            expectedFail = "Aborting: Unable to parse signature file: Incompatible class Test.pkg.Class1 definitions"
         )
     }
 
diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
index c206082..4a5c6d1 100644
--- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
+++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
@@ -3355,7 +3355,7 @@
         // Regression test for 130567941
         check(
             expectedIssues = """
-            TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert has changed return type from Number to java.lang.Number [ChangedType]
+            TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert1 has changed return type from Number to java.lang.Number [ChangedType]
             """,
             inputKotlinStyleNulls = true,
             outputKotlinStyleNulls = true,
@@ -3364,7 +3364,7 @@
                 package test.pkg.sample {
                   public abstract class SampleClass {
                     method public <Number> Number! convert(Number);
-                    method public <Number> Number! convert(Number);
+                    method public <Number> Number! convert1(Number);
                   }
                 }
                 """,
@@ -3375,7 +3375,7 @@
                     // Here the generic type parameter applies to both the function argument and the function return type
                     method public <Number> Number! convert(Number);
                     // Here the generic type parameter applies to the function argument but not the function return type
-                    method public <Number> java.lang.Number! convert(Number);
+                    method public <Number> java.lang.Number! convert1(Number);
                   }
                 }
             """