Skip to content

Commit 879a1fe

Browse files
natiginfogithub-actions[bot]
authored andcommitted
Accept changes to companion object field with excluded annotations (#3742)
GitOrigin-RevId: fcfe0a4646c56dd3243057f1e1ec1e34b080fc17
1 parent e6bf5d5 commit 879a1fe

File tree

1 file changed

+69
-0
lines changed

1 file changed

+69
-0
lines changed

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import japicmp.model.JApiAnnotation
55
import japicmp.model.JApiClass
66
import japicmp.model.JApiCompatibility
77
import japicmp.model.JApiCompatibilityChangeType
8+
import japicmp.model.JApiField
89
import japicmp.model.JApiMethod
10+
import javassist.CtMethod
911
import me.champeau.gradle.japicmp.JapicmpTask
1012
import me.champeau.gradle.japicmp.report.Violation
1113
import me.champeau.gradle.japicmp.report.stdrules.AbstractRecordingSeenMembers
@@ -224,6 +226,10 @@ public class InternalFilterRule : AbstractRecordingSeenMembers() {
224226
Violation.accept(member, "Kotlin internal visibility")
225227
} else if (member.hasOnlyKotlinMetadataModification()) {
226228
Violation.accept(member, "Kotlin metadata change")
229+
} else if(member.classContainsOnlyMethodChangesWithExcludedAnnotations()) {
230+
Violation.accept(member, "Contains changes to excluded annotations")
231+
} else if(member.containsChangesToCompanionObjectVariableWithExcludedAnnotations()) {
232+
Violation.accept(member, "Companion object variable with excluded annotations")
227233
} else {
228234
binaryIncompatibleRule.maybeAddViolation(member)
229235
}
@@ -267,6 +273,69 @@ public class InternalFilterRule : AbstractRecordingSeenMembers() {
267273
private fun List<JApiAnnotation>?.containsKotlinMetadata(): Boolean {
268274
return this?.any { it.fullyQualifiedName == "kotlin.Metadata" } == true
269275
}
276+
277+
/**
278+
* When we have a constant declared in a companion object, the annotation is declared in separate class ending with '$Companion'.
279+
* This function checks if field is declared in companion object and if it has any of the excluded annotations.
280+
*/
281+
private fun JApiCompatibility.containsChangesToCompanionObjectVariableWithExcludedAnnotations(): Boolean {
282+
if (this.isBinaryCompatible) return false
283+
val member = (this as? JApiField) ?: return false
284+
// Get the declaring class first
285+
val declaringClass = member.getjApiClass()
286+
val oldClass = declaringClass.oldClass.orElse(null) ?: return false
287+
val companionClass =
288+
oldClass.declaredClasses.firstOrNull { it.name.contains("${oldClass.name}\$Companion") }
289+
?: return false
290+
val methodMatchingFieldName =
291+
companionClass.methods.firstOrNull { it.name.contains(member.name) } ?: return false
292+
return methodMatchingFieldName.containsExcludedAnnotation()
293+
}
294+
295+
private fun CtMethod.containsExcludedAnnotation(): Boolean {
296+
return arrayOf(
297+
RESTRICT_TO_ANNOTATION,
298+
MAPS_EXPERIMENTAL_ANNOTATION,
299+
VISIBLE_FOR_TESTING_ANNOTATION,
300+
EXPERIMENTAL_ANNOTATION
301+
)
302+
.map { it.substring(1) }
303+
.any {
304+
hasAnnotation(it)
305+
}
306+
}
307+
308+
/**
309+
* Checks if all method updates in a class and its declared classes only affect methods with excluded annotations.
310+
*
311+
* This function identifies methods that existed in the old version of a class and its nested classes
312+
* but were either removed or changed in the new version. It goes through updated methods
313+
* and checks whether they contain excluded annotations (like @MapboxExperimental, @RestrictTo, etc.).
314+
*
315+
* @return true if all method changes only affect methods with excluded annotations.
316+
*/
317+
private fun JApiCompatibility.classContainsOnlyMethodChangesWithExcludedAnnotations(): Boolean {
318+
if (this.isBinaryCompatible) return false
319+
val member = (this as? JApiClass) ?: return false
320+
// Get the declaring class first
321+
val declaringClass = member
322+
val oldClass = declaringClass.oldClass.orElse(null) ?: return false
323+
val newClass = declaringClass.newClass.orElse(null) ?: return false
324+
val oldMethods = (oldClass.methods + oldClass.declaredClasses.flatMap { it.methods.toList() }).toMutableList()
325+
val newMethods = newClass.methods + newClass.declaredClasses.flatMap { it.methods.toList() }
326+
newMethods.forEach { newMethod ->
327+
// remove methods that have the same name and signature
328+
oldMethods.removeIf {
329+
it.name == newMethod.name && it.signature == newMethod.signature
330+
}
331+
}
332+
// Remaining methods might be deleted or have been changed.
333+
// If they contain excluded annotations, we will not consider them as breaking changes.
334+
// That's why we check if all of them contain excluded annotations.
335+
// If there is at least one method that doesn't contain excluded annotation,
336+
// we will consider it as breaking change and JApiCmp should provide details in the report.
337+
return oldMethods.all { it.containsExcludedAnnotation() }
338+
}
270339
}
271340

272341
private const val RESTRICT_TO_ANNOTATION = "@androidx.annotation.RestrictTo"

0 commit comments

Comments
 (0)