22// SPDX-License-Identifier: Apache-2.0
33package com.autonomousapps.internal.binary
44
5+ import com.autonomousapps.internal.strings.dotty
56import com.autonomousapps.internal.unsafeLazy
7+ import com.autonomousapps.internal.utils.efficient
68import com.autonomousapps.internal.utils.filterToOrderedSet
79import com.autonomousapps.internal.utils.mapToOrderedSet
810import com.autonomousapps.internal.utils.mapToSet
@@ -13,6 +15,10 @@ import com.autonomousapps.model.internal.intermediates.consumer.MemberAccess
1315import com.autonomousapps.model.internal.intermediates.producer.BinaryClass
1416import com.autonomousapps.visitor.GraphViewVisitor
1517
18+ /* *
19+ * TODO(tsr): there are [reports](https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1604) that
20+ * this analysis is blowing up heap usage and leading to OOMs.
21+ */
1622internal class BinaryCompatibilityChecker (
1723 private val coordinates : Coordinates ,
1824 private val binaryClassCapability : BinaryClassCapability ,
@@ -25,6 +31,28 @@ internal class BinaryCompatibilityChecker(
2531 val isBinaryCompatible : Boolean ,
2632 )
2733
34+ private data class PartitionResult (
35+ val matchingClasses : Set <BinaryClass >,
36+ val nonMatchingClasses : Set <BinaryClass >,
37+ ) {
38+
39+ companion object {
40+ fun empty (): PartitionResult = PartitionResult (emptySet(), emptySet())
41+ }
42+
43+ class Builder {
44+ val matchingClasses = sortedSetOf<BinaryClass >()
45+ val nonMatchingClasses = sortedSetOf<BinaryClass >()
46+
47+ fun build (): PartitionResult {
48+ return PartitionResult (
49+ matchingClasses = matchingClasses.efficient(),
50+ nonMatchingClasses = nonMatchingClasses.efficient(),
51+ )
52+ }
53+ }
54+ }
55+
2856 val result: Result ? by unsafeLazy { compute() }
2957
3058 private fun compute (): Result ? {
@@ -77,7 +105,7 @@ internal class BinaryCompatibilityChecker(
77105 )
78106 }
79107
80- private fun Set<BinaryClassCapability. PartitionResult>.reduce (): BinaryClassCapability . PartitionResult {
108+ private fun Set<PartitionResult>.reduce (): PartitionResult {
81109 val matches = sortedSetOf<BinaryClass >()
82110 val nonMatches = sortedSetOf<BinaryClass >()
83111
@@ -86,7 +114,7 @@ internal class BinaryCompatibilityChecker(
86114 nonMatches.addAll(result.nonMatchingClasses)
87115 }
88116
89- return BinaryClassCapability . PartitionResult (
117+ return PartitionResult (
90118 matchingClasses = matches.reduce(),
91119 nonMatchingClasses = nonMatches.reduce(),
92120 )
@@ -115,4 +143,95 @@ internal class BinaryCompatibilityChecker(
115143
116144 return builders.values.mapToOrderedSet { it.build() }
117145 }
146+
147+ private fun BinaryClassCapability.findMatchingClasses (memberAccess : MemberAccess ): PartitionResult {
148+ val relevant = findRelevantBinaryClasses(memberAccess)
149+
150+ // lenient
151+ if (relevant.isEmpty()) return PartitionResult .empty()
152+
153+ return relevant
154+ .map { bin -> bin.partition(memberAccess) }
155+ .fold(PartitionResult .Builder ()) { acc, (match, nonMatch) ->
156+ acc.apply {
157+ match?.let { matchingClasses.add(it) }
158+ nonMatch?.let { nonMatchingClasses.add(it) }
159+ }
160+ }
161+ .build()
162+ }
163+
164+ /* *
165+ * Example:
166+ * 1. [memberAccess] is for `groovy/lang/MetaClass#getProperty`.
167+ * 2. That method is actually provided by `groovy/lang/MetaObjectProtocol`, which `groovy/lang/MetaClass` implements.
168+ *
169+ * All of the above ("this" class, its super class, and its interfaces) are relevant for search purposes. Note we
170+ * don't inspect the member names for this check.
171+ */
172+ private fun BinaryClassCapability.findRelevantBinaryClasses (memberAccess : MemberAccess ): Set <BinaryClass > {
173+ // direct references
174+ val relevant = binaryClasses.filterTo(mutableSetOf ()) { bin ->
175+ bin.className == memberAccess.owner.dotty()
176+ }
177+
178+ // Walk up the class hierarchy
179+ fun walkUp (): Int {
180+ binaryClasses.filterTo(relevant) { bin ->
181+ bin.className in relevant.map { it.superClassName }
182+ || bin.className in relevant.flatMap { it.interfaces }
183+ }
184+ return relevant.size
185+ }
186+
187+ // TODO(tsr): this could be more performant
188+ do {
189+ val size = relevant.size
190+ val newSize = walkUp()
191+ } while (newSize > size)
192+
193+ return relevant
194+ }
195+
196+ /* *
197+ * Partitions and returns artificial pair of [BinaryClasses][BinaryClass]. Non-null elements indicate relevant (to
198+ * [memberAccess]) matching and non-matching members of this `BinaryClass`. Matching members are binary-compatible;
199+ * and non-matching members have the same [name][com.autonomousapps.model.internal.intermediates.producer.Member.name]
200+ * but incompatible [descriptors][com.autonomousapps.model.internal.intermediates.producer.Member.descriptor], and are
201+ * therefore binary-incompatible.
202+ *
203+ * nb: We don't want this as a method directly in BinaryClass because it can't safely assert the prerequisite that
204+ * it's only called on "relevant" classes. THIS class, however, can, via [findRelevantBinaryClasses].
205+ */
206+ private fun BinaryClass.partition (memberAccess : MemberAccess ): Pair <BinaryClass ?, BinaryClass ?> {
207+ // There can be only one match
208+ val matchingFields = effectivelyPublicFields.firstOrNull { it.matches(memberAccess) }
209+ val matchingMethods = effectivelyPublicMethods.firstOrNull { it.matches(memberAccess) }
210+
211+ // There can be many non-matches
212+ val nonMatchingFields = effectivelyPublicFields.filterToOrderedSet { it.doesNotMatch(memberAccess) }
213+ val nonMatchingMethods = effectivelyPublicMethods.filterToOrderedSet { it.doesNotMatch(memberAccess) }
214+
215+ // Create a view of the binary class containing only the matching members.
216+ val match = if (matchingFields != null || matchingMethods != null ) {
217+ copy(
218+ effectivelyPublicFields = matchingFields?.let { setOf (it) }.orEmpty(),
219+ effectivelyPublicMethods = matchingMethods?.let { setOf (it) }.orEmpty()
220+ )
221+ } else {
222+ null
223+ }
224+
225+ // Create a view of the binary class containing only the non-matching members.
226+ val nonMatch = if (nonMatchingFields.isNotEmpty() || nonMatchingMethods.isNotEmpty()) {
227+ copy(
228+ effectivelyPublicFields = nonMatchingFields,
229+ effectivelyPublicMethods = nonMatchingMethods,
230+ )
231+ } else {
232+ null
233+ }
234+
235+ return match to nonMatch
236+ }
118237}
0 commit comments