11package com.mapbox.maps.gradle.plugins.extensions
22
33import 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
99import 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
1014import org.gradle.api.Project
1115import org.gradle.api.model.ObjectFactory
1216import org.gradle.api.provider.Property
@@ -18,7 +22,6 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin
1822import java.nio.file.Files
1923import javax.inject.Inject
2024
21-
2225public 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