Skip to content

Commit 1b47df9

Browse files
feat: special support for @composable when doing binary compatibility checks.
1 parent a1f3b5b commit 1b47df9

File tree

4 files changed

+82
-41
lines changed

4 files changed

+82
-41
lines changed

src/main/kotlin/com/autonomousapps/internal/asm.kt

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import com.autonomousapps.internal.kotlin.AccessFlags
99
import com.autonomousapps.internal.utils.METHOD_DESCRIPTOR_REGEX
1010
import com.autonomousapps.internal.utils.efficient
1111
import com.autonomousapps.internal.utils.genericTypes
12-
import com.autonomousapps.model.internal.intermediates.producer.Member
1312
import com.autonomousapps.model.internal.intermediates.consumer.MemberAccess
13+
import com.autonomousapps.model.internal.intermediates.producer.Member
1414
import kotlinx.metadata.jvm.Metadata
1515
import org.gradle.api.logging.Logger
1616
import java.util.SortedSet
@@ -19,7 +19,10 @@ import java.util.concurrent.atomic.AtomicReference
1919
private val logDebug: Boolean get() = Flags.logBytecodeDebug()
2020
private const val ASM_VERSION = Opcodes.ASM9
2121

22-
/** This will collect the class name and information about annotations. */
22+
/**
23+
* This will collect the class name and information about annotations. It is used on the "producer" side to create
24+
* the [ExplodedJar][com.autonomousapps.model.internal.intermediates.producer.ExplodedJar] model, via [AnalyzedClass].
25+
*/
2326
internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : ClassVisitor(ASM_VERSION) {
2427

2528
private lateinit var className: String
@@ -98,7 +101,7 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
98101

99102
override fun visitMethod(
100103
access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?
101-
): MethodVisitor? {
104+
): MethodVisitor {
102105
log { "- visitMethod: ${Access.fromInt(access)} descriptor=$descriptor name=$name signature=$signature" }
103106

104107
if (!("()V" == descriptor && ("<init>" == name || "<clinit>" == name))) {
@@ -107,17 +110,17 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
107110
methods.add(Method(descriptor))
108111
}
109112

113+
var method: Member.Method? = null
110114
if (isEffectivelyPublic(access)) {
111-
effectivelyPublicMethods.add(
112-
Member.Method(
113-
access = access,
114-
name = name,
115-
descriptor = descriptor,
116-
)
115+
method = Member.Method(
116+
access = access,
117+
name = name,
118+
descriptor = descriptor,
117119
)
120+
effectivelyPublicMethods.add(method)
118121
}
119122

120-
return null
123+
return MethodAnalyzer(logger, effectivelyPublicMethods, method)
121124
}
122125

123126
override fun visitField(
@@ -189,6 +192,43 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
189192
}
190193
}
191194
}
195+
196+
private class MethodAnalyzer(
197+
private val logger: Logger,
198+
private val effectivelyPublicMethods: MutableSet<Member.Method>,
199+
private val method: Member.Method?,
200+
) : MethodVisitor(ASM_VERSION) {
201+
202+
private fun log(msgProvider: () -> String) {
203+
if (!logDebug) {
204+
logger.quiet(msgProvider())
205+
}
206+
}
207+
208+
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
209+
log { "MethodAnalyzer#visitAnnotation: descriptor=$descriptor visible=$visible" }
210+
211+
// For @Composable functions, we also add an artificial version that matches what the compose Kotlin compiler
212+
// plugin generates. For example:
213+
//
214+
// * In source, the `isSystemInDarkTheme()` function seems to have the descriptor `()Z` (takes no arguments,
215+
// returns a boolean).
216+
// * In the generated bytecode, the descriptor is `(Landroidx/compose/runtime/Composer;I)Z` (takes a Composer and
217+
// an int, returns a boolean).
218+
if (descriptor == "Landroidx/compose/runtime/Composable;") {
219+
effectivelyPublicMethods
220+
.find { method == it }
221+
?.let { m ->
222+
// remove the original
223+
effectivelyPublicMethods.remove(m)
224+
// and add a new version with the special property set
225+
effectivelyPublicMethods.add(m.copy(special = Member.SPECIAL_COMPOSABLE))
226+
}
227+
}
228+
229+
return null
230+
}
231+
}
192232
}
193233

194234
internal data class ClassRef(

src/main/kotlin/com/autonomousapps/model/internal/Capability.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ internal data class BinaryClassCapability(
188188
/**
189189
* Partitions and returns artificial pair of [BinaryClasses][BinaryClass]. Non-null elements indicate relevant (to
190190
* [memberAccess] matching and non-matching members of this `BinaryClass`. Matching members are binary-compatible; and
191-
* non-matching members have the same [name][com.autonomousapps.model.intermediates.producer.Member.name] but
192-
* incompatible [descriptors][com.autonomousapps.model.intermediates.producer.Member.descriptor], and are therefore
193-
* binary-incompatible.
191+
* non-matching members have the same [name][com.autonomousapps.model.internal.intermediates.producer.Member.name] but
192+
* incompatible [descriptors][com.autonomousapps.model.internal.intermediates.producer.Member.descriptor], and are
193+
* therefore binary-incompatible.
194194
*
195195
* nb: We don't want this as a method directly in BinaryClass because it can't safely assert the prerequisite that
196196
* it's only called on "relevant" classes. THIS class, however, can, via findRelevantBinaryClasses.

src/main/kotlin/com/autonomousapps/model/internal/intermediates/producer/Member.kt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ import dev.zacsweers.moshix.sealed.annotations.TypeLabel
1111
* Represents a member of a [class][BinaryClass].
1212
*
1313
* nb: Borrowing heavily from `asmUtils.kt` and similar but substantially different from
14-
* [MemberAccess][com.autonomousapps.model.intermediates.consumer.MemberAccess] on the consumer side.
14+
* [MemberAccess][com.autonomousapps.model.internal.intermediates.consumer.MemberAccess] on the consumer side.
1515
*/
1616
@JsonClass(generateAdapter = false, generator = "sealed:type")
1717
internal sealed class Member(
1818
open val access: Int,
1919
open val name: String,
2020
open val descriptor: String,
21+
open val special: String?,
2122
) : Comparable<Member> {
2223

24+
companion object {
25+
const val SPECIAL_COMPOSABLE = "@Composable"
26+
}
27+
2328
internal class Printable(
2429
val className: String,
2530
val memberName: String,
@@ -51,14 +56,23 @@ internal sealed class Member(
5156
.compare(this, other)
5257
}
5358

54-
/** Returns true for matching name and descriptor. */
59+
/**
60+
* Returns true for matching name and descriptor, unless [special] == [SPECIAL_COMPOSABLE], in which case we relax the
61+
* requirement that the descriptors match. `@Composable` functions get special attention from a Kotlin compiler
62+
* plugin, such that the source and bytecode do not match.
63+
*/
5564
fun matches(memberAccess: MemberAccess): Boolean {
56-
return name == memberAccess.name && descriptor == memberAccess.descriptor
65+
return name == memberAccess.name
66+
&& (descriptor == memberAccess.descriptor || special == SPECIAL_COMPOSABLE)
5767
}
5868

59-
/** Returns true for matching name and non-matching descriptor. */
69+
/**
70+
* Returns true for matching name and non-matching descriptor, unless [special] == [SPECIAL_COMPOSABLE], in which case
71+
* we can't say that [memberAccess] doesn't match.
72+
*/
6073
fun doesNotMatch(memberAccess: MemberAccess): Boolean {
61-
return name == memberAccess.name && descriptor != memberAccess.descriptor
74+
return name == memberAccess.name
75+
&& (descriptor != memberAccess.descriptor && special != SPECIAL_COMPOSABLE)
6276
}
6377

6478
protected val accessFlags get() = AccessFlags(access)
@@ -78,10 +92,12 @@ internal sealed class Member(
7892
override val access: Int,
7993
override val name: String,
8094
override val descriptor: String,
95+
override val special: String? = null,
8196
) : Member(
8297
access = access,
8398
name = name,
8499
descriptor = descriptor,
100+
special = special,
85101
) {
86102
override val signature: String
87103
get() = "${accessFlags.getModifierString()} fun $name $descriptor"
@@ -93,10 +109,12 @@ internal sealed class Member(
93109
override val access: Int,
94110
override val name: String,
95111
override val descriptor: String,
112+
override val special: String? = null,
96113
) : Member(
97114
access = access,
98115
name = name,
99116
descriptor = descriptor,
117+
special = special,
100118
) {
101119
override val signature: String
102120
get() = "${accessFlags.getModifierString()} field $name $descriptor"

src/main/kotlin/com/autonomousapps/tasks/ComputeUsagesTask.kt

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,11 @@
33
package com.autonomousapps.tasks
44

55
import com.autonomousapps.internal.utils.*
6-
import com.autonomousapps.model.*
6+
import com.autonomousapps.model.Coordinates
7+
import com.autonomousapps.model.DuplicateClass
78
import com.autonomousapps.model.declaration.internal.Bucket
89
import com.autonomousapps.model.declaration.internal.Declaration
9-
import com.autonomousapps.model.internal.AndroidAssetCapability
10-
import com.autonomousapps.model.internal.AndroidLinterCapability
11-
import com.autonomousapps.model.internal.AndroidManifestCapability
12-
import com.autonomousapps.model.internal.AndroidResCapability
13-
import com.autonomousapps.model.internal.AndroidResSource
14-
import com.autonomousapps.model.internal.AnnotationProcessorCapability
15-
import com.autonomousapps.model.internal.BinaryClassCapability
16-
import com.autonomousapps.model.internal.ClassCapability
17-
import com.autonomousapps.model.internal.ConstantCapability
18-
import com.autonomousapps.model.internal.Dependency
19-
import com.autonomousapps.model.internal.DependencyGraphView
20-
import com.autonomousapps.model.internal.InferredCapability
21-
import com.autonomousapps.model.internal.InlineMemberCapability
22-
import com.autonomousapps.model.internal.NativeLibCapability
23-
import com.autonomousapps.model.internal.ProjectVariant
24-
import com.autonomousapps.model.internal.SecurityProviderCapability
25-
import com.autonomousapps.model.internal.ServiceLoaderCapability
26-
import com.autonomousapps.model.internal.TypealiasCapability
10+
import com.autonomousapps.model.internal.*
2711
import com.autonomousapps.model.internal.intermediates.DependencyTraceReport
2812
import com.autonomousapps.model.internal.intermediates.DependencyTraceReport.Kind
2913
import com.autonomousapps.model.internal.intermediates.Reason
@@ -422,7 +406,6 @@ private class GraphVisitor(
422406
// Can't be incompatible if the code compiles in the context of no duplication
423407
if (context.duplicateClasses.isEmpty()) return
424408

425-
// TODO(tsr): special handling for @Composable
426409
val memberAccessOwners = context.project.memberAccesses.mapToSet { it.owner }
427410
val relevantDuplicates = context.duplicateClasses
428411
.filter { duplicate -> coordinates in duplicate.dependencies && duplicate.className in memberAccessOwners }
@@ -435,9 +418,9 @@ private class GraphVisitor(
435418
val relevantMemberAccesses = context.project.memberAccesses
436419
.filterToOrderedSet { access -> access.owner in relevantDuplicateClassNames }
437420

438-
val partitionResult = relevantMemberAccesses.mapToSet { access ->
439-
binaryClassCapability.findMatchingClasses(access)
440-
}.reduce()
421+
val partitionResult = relevantMemberAccesses
422+
.mapToSet { access -> binaryClassCapability.findMatchingClasses(access) }
423+
.reduce()
441424
val matchingBinaryClasses = partitionResult.matchingClasses
442425
val nonMatchingBinaryClasses = partitionResult.nonMatchingClasses
443426

0 commit comments

Comments
 (0)