Repository: Kotlin/binary-compatibility-validator (kotlin)

Issue:
Filter out `$suspendImpl` functions declared in interfaces
On JVM, the compiler generates special `<original function name>$suspendImpl` methods for every open suspendable function. For classes, these methods are package private and are never included in dump. For interfaces, however, such functions could only be either private (but we need to call them outside the interface, so that doesn't work), or public (we are here now). So, these public functions do not constitute the public ABI and are public only due to restrictions imposed on class files.

BCV should filter these methods out of generated dumps.

Pull request description:
Closes #270

Patch:
diff --git a/build.gradle.kts b/build.gradle.kts
index e75f2758..22bc1a99 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -109,6 +109,7 @@ kotlin {
 tasks.compileTestKotlin {
     compilerOptions {
         languageVersion.set(KotlinVersion.KOTLIN_1_9)
+        freeCompilerArgs.add("-Xjvm-default=all-compatibility")
     }
 }
 
diff --git a/src/main/kotlin/api/KotlinMetadataSignature.kt b/src/main/kotlin/api/KotlinMetadataSignature.kt
index d5b0fc72..0a531679 100644
--- a/src/main/kotlin/api/KotlinMetadataSignature.kt
+++ b/src/main/kotlin/api/KotlinMetadataSignature.kt
@@ -60,6 +60,7 @@ internal data class MethodBinarySignature(
         super.isEffectivelyPublic(classAccess, classVisibility)
                 && !isAccessOrAnnotationsMethod()
                 && !isDummyDefaultConstructor()
+                && !isSuspendImplMethod()
 
     override fun findMemberVisibility(classVisibility: ClassVisibility?): MemberVisibility? {
         return super.findMemberVisibility(classVisibility)
@@ -71,6 +72,14 @@ internal data class MethodBinarySignature(
 
     private fun isDummyDefaultConstructor() =
         access.isSynthetic && name == "<init>" && desc == "(Lkotlin/jvm/internal/DefaultConstructorMarker;)V"
+
+    /**
+     * Kotlin compiler emits special `<originalFunctionName>$suspendImpl` methods for open
+     * suspendable functions. These synthetic functions could only be invoked from original function,
+     * or from a corresponding continuation. They don't constitute class's public ABI, but in some cases
+     * they might be declared public (namely, in case of default interface methods).
+     */
+    private fun isSuspendImplMethod() = access.isSynthetic && name.endsWith("\$suspendImpl")
 }
 
 /**


Test patch:
diff --git a/src/test/kotlin/cases/default/default.txt b/src/test/kotlin/cases/default/default.txt
index 6e6eb8a9..2a095fb3 100644
--- a/src/test/kotlin/cases/default/default.txt
+++ b/src/test/kotlin/cases/default/default.txt
@@ -7,7 +7,9 @@ public class cases/default/ClassFunctions {
 
 public abstract interface class cases/default/InterfaceFunctions {
 	public abstract fun withAllDefaults (ILjava/lang/String;)V
+	public static synthetic fun withAllDefaults$default (Lcases/default/InterfaceFunctions;ILjava/lang/String;ILjava/lang/Object;)V
 	public abstract fun withSomeDefaults (ILjava/lang/String;)V
+	public static synthetic fun withSomeDefaults$default (Lcases/default/InterfaceFunctions;ILjava/lang/String;ILjava/lang/Object;)V
 }
 
 public final class cases/default/InterfaceFunctions$DefaultImpls {
diff --git a/src/test/kotlin/cases/interfaces/interfaces.txt b/src/test/kotlin/cases/interfaces/interfaces.txt
index 4f37b42f..0e4cecf0 100644
--- a/src/test/kotlin/cases/interfaces/interfaces.txt
+++ b/src/test/kotlin/cases/interfaces/interfaces.txt
@@ -1,5 +1,5 @@
 public abstract interface class cases/interfaces/BaseWithImpl {
-	public abstract fun foo ()I
+	public fun foo ()I
 }
 
 public final class cases/interfaces/BaseWithImpl$DefaultImpls {
@@ -7,7 +7,7 @@ public final class cases/interfaces/BaseWithImpl$DefaultImpls {
 }
 
 public abstract interface class cases/interfaces/DerivedWithImpl : cases/interfaces/BaseWithImpl {
-	public abstract fun foo ()I
+	public fun foo ()I
 }
 
 public final class cases/interfaces/DerivedWithImpl$DefaultImpls {
diff --git a/src/test/kotlin/cases/suspend/suspend.kt b/src/test/kotlin/cases/suspend/suspend.kt
new file mode 100644
index 00000000..b54b8825
--- /dev/null
+++ b/src/test/kotlin/cases/suspend/suspend.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2016-2024 JetBrains s.r.o.
+ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
+ */
+
+package cases.suspend
+
+public interface I {
+    suspend fun openFunction(): Int = 42
+}
+
+public interface II : I {
+    override suspend fun openFunction(): Int {
+        return super.openFunction() + 1
+    }
+}
+
+public open class C : II {
+    override suspend fun openFunction(): Int {
+        return super.openFunction() + 2
+    }
+}
diff --git a/src/test/kotlin/cases/suspend/suspend.txt b/src/test/kotlin/cases/suspend/suspend.txt
new file mode 100644
index 00000000..295410d5
--- /dev/null
+++ b/src/test/kotlin/cases/suspend/suspend.txt
@@ -0,0 +1,21 @@
+public class cases/suspend/C : cases/suspend/II {
+	public fun <init> ()V
+	public fun openFunction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class cases/suspend/I {
+	public fun openFunction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class cases/suspend/I$DefaultImpls {
+	public static fun openFunction (Lcases/suspend/I;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class cases/suspend/II : cases/suspend/I {
+	public fun openFunction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class cases/suspend/II$DefaultImpls {
+	public static fun openFunction (Lcases/suspend/II;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
diff --git a/src/test/kotlin/tests/CasesPublicAPITest.kt b/src/test/kotlin/tests/CasesPublicAPITest.kt
index d7949b0e..e97d95e3 100644
--- a/src/test/kotlin/tests/CasesPublicAPITest.kt
+++ b/src/test/kotlin/tests/CasesPublicAPITest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2020 JetBrains s.r.o.
+ * Copyright 2016-2024 JetBrains s.r.o.
  * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
  */
 
@@ -58,6 +58,8 @@ class CasesPublicAPITest {
 
     @Test fun special() { snapshotAPIAndCompare(testName.methodName) }
 
+    @Test fun suspend() { snapshotAPIAndCompare(testName.methodName) }
+
     @Test fun whenMappings() { snapshotAPIAndCompare(testName.methodName) }
 
     @Test fun enums() { snapshotAPIAndCompare(testName.methodName) }