Skip to content

Commit 7275216

Browse files
natiginfogithub-actions[bot]
authored andcommitted
Fix binary compatibility checks for maps-android (#3528)
Changes: 1. We don't need to check binary compatibility issues outside PR because once PR is merged, it means compatibility issues were already accepted. 2. Use `annotationExcludes` to exclude items marked with certain annotations where binary changes are acceptable. 3. Implement `InternalFilterRule` which will ignore changes in methods / properties with `internal` visibility. 4. Bumped japicmp plugin version. cc @mapbox/sdk-ci GitOrigin-RevId: d17e073450ccbb6cf32540ce9fdb0ed66da423c7
1 parent b07b9ac commit 7275216

File tree

2 files changed

+75
-55
lines changed

2 files changed

+75
-55
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pluginBinaryCompatibilityValidator = "0.13.2"
3030
dokka = "1.7.20"
3131
kotlin = "1.7.20"
3232
detekt = "1.22.0"
33-
japicmp = "0.4.1"
33+
japicmp = "0.4.6"
3434

3535
# Dependencies
3636

mapbox-convention-plugin/src/main/kotlin/com/mapbox/maps/gradle/plugins/extensions/MapboxJApiCmpExtension.kt

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.mapbox.maps.gradle.plugins.extensions
22

33
import com.mapbox.maps.gradle.plugins.internal.setDisallowChanges
4-
import japicmp.filter.BehaviorFilter
5-
import japicmp.filter.ClassFilter
6-
import javassist.CtBehavior
7-
import javassist.CtClass
8-
import javassist.NotFoundException
4+
import japicmp.model.JApiAnnotation
5+
import japicmp.model.JApiClass
6+
import japicmp.model.JApiCompatibility
7+
import japicmp.model.JApiCompatibilityChangeType
8+
import japicmp.model.JApiMethod
99
import me.champeau.gradle.japicmp.JapicmpTask
10+
import me.champeau.gradle.japicmp.report.Violation
11+
import me.champeau.gradle.japicmp.report.stdrules.AbstractRecordingSeenMembers
12+
import me.champeau.gradle.japicmp.report.stdrules.BinaryIncompatibleRule
13+
import me.champeau.gradle.japicmp.report.stdrules.RecordSeenMembersSetup
1014
import org.gradle.api.Project
1115
import org.gradle.api.model.ObjectFactory
1216
import org.gradle.api.provider.Property
@@ -18,7 +22,6 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin
1822
import java.nio.file.Files
1923
import javax.inject.Inject
2024

21-
2225
public abstract class MapboxJApiCmpExtension @Inject constructor(objects: ObjectFactory) {
2326
private val currentVersionProperty: Property<String> = objects.property<String>()
2427
private val previousVersionProperty: Property<String> = objects.property<String>()
@@ -179,15 +182,20 @@ public abstract class MapboxJApiCmpExtension @Inject constructor(objects: Object
179182
//includeSynthetic.set(true)
180183
ignoreMissingClasses.set(true)
181184

182-
// Exclude classes and/or methods annotated with RestrictTo
183-
addExcludeFilter(ClassWithRestrictToAnnotationFilter::class.java)
184-
addExcludeFilter(MethodWithRestrictToAnnotationFilter::class.java)
185-
186-
// Exclude classes and/or methods annotated with MapboxExperimental
187-
addExcludeFilter(ClassWithExperimentalAnnotationFilter::class.java)
188-
addExcludeFilter(MethodWithExperimentalAnnotationFilter::class.java)
185+
// Exclude @RestrictTo, @VisibleForTesting and @MapboxExperimental annotations
186+
annotationExcludes.set(
187+
listOf(
188+
RESTRICT_TO_ANNOTATION,
189+
EXPERIMENTAL_ANNOTATION,
190+
MAPS_EXPERIMENTAL_ANNOTATION,
191+
VISIBLE_FOR_TESTING_ANNOTATION,
192+
)
193+
)
189194

190195
richReport {
196+
addDefaultRules.set(true)
197+
addSetupRule(RecordSeenMembersSetup::class.java)
198+
addRule(InternalFilterRule::class.java)
191199
destinationDir.set(rootProject.layout.buildDirectory.dir("reports/japi/"))
192200
reportName.set("${project.path.drop(1)}.html")
193201
}
@@ -203,53 +211,65 @@ public abstract class MapboxJApiCmpExtension @Inject constructor(objects: Object
203211
}
204212
}
205213

206-
private const val RESTRICT_TO_ANNOTATION_NAME = "androidx.annotation.RestrictTo"
207-
private const val EXPERIMENTAL_ANNOTATION_NAME = "com.mapbox.maps.MapboxExperimental"
208214

209-
public class ClassWithRestrictToAnnotationFilter : ClassFilter {
210-
override fun matches(ctClass: CtClass): Boolean =
211-
ctClass.itOrDeclaringClassHasAnnotation(RESTRICT_TO_ANNOTATION_NAME)
212-
}
213-
214-
public class ClassWithExperimentalAnnotationFilter : ClassFilter {
215-
override fun matches(ctClass: CtClass): Boolean =
216-
ctClass.hasAnnotation(EXPERIMENTAL_ANNOTATION_NAME)
217-
}
218-
219-
public class MethodWithExperimentalAnnotationFilter : BehaviorFilter {
220-
override fun matches(ctBehavior: CtBehavior): Boolean =
221-
ctBehavior.itOrDeclaringClassHasAnnotation(EXPERIMENTAL_ANNOTATION_NAME)
222-
}
215+
/**
216+
* Changes to non-internal properties and changing their visibility to `internal` is a breaking change.
217+
* However, changes to the internal variables are not considered as a breaking change.
218+
*/
219+
public class InternalFilterRule : AbstractRecordingSeenMembers() {
220+
private val binaryIncompatibleRule = BinaryIncompatibleRule()
221+
222+
override fun maybeAddViolation(member: JApiCompatibility): Violation? {
223+
return if (member.containsInternallyVisibleFunctionOrVariableChanges()) {
224+
Violation.accept(member, "Kotlin internal visibility")
225+
} else if (member.hasOnlyKotlinMetadataModification()) {
226+
Violation.accept(member, "Kotlin metadata change")
227+
} else {
228+
binaryIncompatibleRule.maybeAddViolation(member)
229+
}
230+
}
223231

224-
public class MethodWithRestrictToAnnotationFilter : BehaviorFilter {
225-
override fun matches(ctBehavior: CtBehavior): Boolean =
226-
ctBehavior.itOrDeclaringClassHasAnnotation(RESTRICT_TO_ANNOTATION_NAME)
227-
}
232+
/**
233+
* This function checks if the given JApiCompatibility object has a change to an internal variable or function.
234+
*/
235+
private fun JApiCompatibility.containsInternallyVisibleFunctionOrVariableChanges(): Boolean {
236+
if (this.isBinaryCompatible) return false
237+
val member = (this as? JApiMethod) ?: return false
238+
val isKotlinClass = member.itOrDeclaringClassContainsKotlinMetadata()
239+
// Check if the method corresponds to an internal variable
240+
return isKotlinClass && member.name.contains("$") && member.accessModifier.valueOld.equals(
241+
"public", ignoreCase = true
242+
)
243+
}
228244

229-
private fun CtBehavior.itOrDeclaringClassHasAnnotation(annotationName: String): Boolean {
230-
var hasAnnotation = hasAnnotation(annotationName)
231-
// Method does not have annotation, let's check its class
232-
try {
233-
if (!hasAnnotation) {
234-
hasAnnotation = declaringClass.itOrDeclaringClassHasAnnotation(annotationName)
245+
/**
246+
* This function checks if the given JApiCompatibility object has a modification to the Kotlin metadata.
247+
* If the change is an annotation modification and the member is a class,
248+
* it checks if the class has only Kotlin metadata changes.
249+
*
250+
* @return true if member is not binary compatible and contains only Kotlin metadata changes, false otherwise.
251+
*/
252+
private fun JApiCompatibility.hasOnlyKotlinMetadataModification(): Boolean {
253+
if (this.isBinaryCompatible) return false
254+
val member = this as? JApiClass
255+
val incompatibleChanges = member?.compatibilityChanges
256+
val compatibilityChange = incompatibleChanges?.firstOrNull()?.takeIf {
257+
it.type == JApiCompatibilityChangeType.ANNOTATION_MODIFIED && incompatibleChanges.size == 1
235258
}
236-
} catch (_: NotFoundException) {
237-
// Not much we can do if we can't load the class so let's ignore it
259+
if (compatibilityChange == null) return false
260+
return member.annotations.containsKotlinMetadata()
238261
}
239-
return hasAnnotation
240-
}
241262

242-
private fun CtClass.itOrDeclaringClassHasAnnotation(annotationName: String): Boolean {
243-
var hasAnnotation = hasAnnotation(annotationName)
244-
if (!hasAnnotation) {
245-
try {
246-
if (declaringClass != null) {
247-
hasAnnotation = declaringClass.itOrDeclaringClassHasAnnotation(annotationName)
248-
}
249-
} catch (_: NotFoundException) {
250-
// Not much we can do if we can't load the class so let's ignore it
251-
}
263+
private fun JApiMethod.itOrDeclaringClassContainsKotlinMetadata(): Boolean {
264+
return this.annotations.containsKotlinMetadata() || this.getjApiClass()?.annotations.containsKotlinMetadata() == true
265+
}
266+
267+
private fun List<JApiAnnotation>?.containsKotlinMetadata(): Boolean {
268+
return this?.any { it.fullyQualifiedName == "kotlin.Metadata" } == true
252269
}
253-
return hasAnnotation
254270
}
255271

272+
private const val RESTRICT_TO_ANNOTATION = "@androidx.annotation.RestrictTo"
273+
private const val MAPS_EXPERIMENTAL_ANNOTATION = "@com.mapbox.maps.MapboxExperimental"
274+
private const val VISIBLE_FOR_TESTING_ANNOTATION = "@androidx.annotation.VisibleForTesting"
275+
private const val EXPERIMENTAL_ANNOTATION = "@com.mapbox.annotation.MapboxExperimental"

0 commit comments

Comments
 (0)