diff --git a/src/main/kotlin/api/KotlinMetadataSignature.kt b/src/main/kotlin/api/KotlinMetadataSignature.kt index 0f3f9ef0..e69ffb83 100644 --- a/src/main/kotlin/api/KotlinMetadataSignature.kt +++ b/src/main/kotlin/api/KotlinMetadataSignature.kt @@ -164,7 +164,8 @@ internal data class AccessFlags(val access: Int) { fun getModifierString(): String = getModifiers().joinToString(" ") } -internal fun FieldBinarySignature.isCompanionField(outerClassMetadata: KotlinClassMetadata?): Boolean { +internal fun FieldNode.isCompanionField(outerClassMetadata: KotlinClassMetadata?): Boolean { + val access = AccessFlags(access) if (!access.isFinal || !access.isStatic) return false val metadata = outerClassMetadata ?: return false // Non-classes are not affected by the problem @@ -172,3 +173,7 @@ internal fun FieldBinarySignature.isCompanionField(outerClassMetadata: KotlinCla return metadata.toKmClass().companionObject == name } +internal fun ClassNode.companionName(outerClassMetadata: KotlinClassMetadata?): String { + val outerKClass = (outerClassMetadata as KotlinClassMetadata.Class).toKmClass() + return name + "$" + outerKClass.companionObject +} diff --git a/src/main/kotlin/api/KotlinSignaturesLoading.kt b/src/main/kotlin/api/KotlinSignaturesLoading.kt index b0a3f60b..8698407c 100644 --- a/src/main/kotlin/api/KotlinSignaturesLoading.kt +++ b/src/main/kotlin/api/KotlinSignaturesLoading.kt @@ -43,54 +43,25 @@ public fun Sequence.loadApiFromJvmClasses(visibilityFilter: (String val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted() val fieldSignatures = fields - .map { - val annotationHolders = - mVisibility?.members?.get(JvmFieldSignature(it.name, it.desc))?.propertyAnnotation - val foundAnnotations = methods.annotationsFor(annotationHolders?.method) - it.toFieldBinarySignature(foundAnnotations) - }.filter { - it.isEffectivelyPublic(classAccess, mVisibility) - }.filter { + .map { it.buildFieldSignature(mVisibility, this, classNodeMap) } + .filter { it.field.isEffectivelyPublic(classAccess, mVisibility) } + .filter { /* * Filter out 'public static final Companion' field that doesn't constitute public API. * For that we first check if field corresponds to the 'Companion' class and then * if companion is effectively public by itself, so the 'Companion' field has the same visibility. */ - if (!it.isCompanionField(classNode.kotlinMetadata)) return@filter true - val outerKClass = (classNode.kotlinMetadata as KotlinClassMetadata.Class).toKmClass() - val companionName = name + "$" + outerKClass.companionObject - // False positive is better than the crash here - val companionClass = classNodeMap[companionName] ?: return@filter true - val visibility = visibilityMap[companionName] ?: return@filter true + val companionClass = when (it) { + is BasicFieldBinarySignature -> return@filter true + is CompanionFieldBinarySignature -> it.companion + } + val visibility = visibilityMap[companionClass.name] ?: return@filter true companionClass.isEffectivelyPublic(visibility) - } + }.map { it.field } // NB: this 'map' is O(methods + properties * methods) which may accidentally be quadratic - val methodSignatures = methods.map { - /** - * For getters/setters, pull the annotations from the property - * This is either on the field if any or in a '$annotations' synthetic function. - */ - val annotationHolders = - mVisibility?.members?.get(JvmMethodSignature(it.name, it.desc))?.propertyAnnotation - val foundAnnotations = ArrayList() - if (annotationHolders != null) { - foundAnnotations += fields.annotationsFor(annotationHolders.field) - foundAnnotations += methods.annotationsFor(annotationHolders.method) - } - - /** - * For synthetic $default methods, pull the annotations from the corresponding method - */ - val alternateDefaultSignature = mVisibility?.name?.let { className -> - it.alternateDefaultSignature(className) - } - foundAnnotations += methods.annotationsFor(alternateDefaultSignature) - - it.toMethodBinarySignature(foundAnnotations, alternateDefaultSignature) - }.filter { - it.isEffectivelyPublic(classAccess, mVisibility) - } + val methodSignatures = methods.map { it.buildMethodSignature(mVisibility, this) } + .filter { it.isEffectivelyPublic(classAccess, mVisibility) } ClassBinarySignature( name, superName, outerClassName, supertypes, fieldSignatures + methodSignatures, classAccess, @@ -102,6 +73,82 @@ public fun Sequence.loadApiFromJvmClasses(visibilityFilter: (String } } +/** + * Wraps a [FieldBinarySignature] along with additional information. + */ +private sealed class FieldBinarySignatureWrapper(val field: FieldBinarySignature) + +/** + * Wraps a regular field's binary signature. + */ +private class BasicFieldBinarySignature(field: FieldBinarySignature) : FieldBinarySignatureWrapper(field) + +/** + * Wraps a binary signature for a field referencing a companion object. + */ +private class CompanionFieldBinarySignature(field: FieldBinarySignature, val companion: ClassNode) : + FieldBinarySignatureWrapper(field) + +private fun FieldNode.buildFieldSignature( + ownerVisibility: ClassVisibility?, + ownerClass: ClassNode, + classes: TreeMap +): FieldBinarySignatureWrapper { + val annotationHolders = + ownerVisibility?.members?.get(JvmFieldSignature(name, desc))?.propertyAnnotation + val foundAnnotations = mutableListOf() + foundAnnotations.addAll(ownerClass.methods.annotationsFor(annotationHolders?.method)) + + var companionClass: ClassNode? = null + if (isCompanionField(ownerClass.kotlinMetadata)) { + /* + * If the field was generated to hold the reference to a companion class's instance, + * then we have to also take all annotations from the companion class an associate it with + * the field. Otherwise, all these annotations will be lost and if the class was marked + * as non-public API using some annotation, then we won't be able to filter out + * the companion field. + */ + val companionName = ownerClass.companionName(ownerClass.kotlinMetadata) + companionClass = classes[companionName] + foundAnnotations.addAll(companionClass?.visibleAnnotations.orEmpty()) + foundAnnotations.addAll(companionClass?.invisibleAnnotations.orEmpty()) + } + + val fieldSignature = toFieldBinarySignature(foundAnnotations) + return if (companionClass != null) { + CompanionFieldBinarySignature(fieldSignature, companionClass) + } else { + BasicFieldBinarySignature(fieldSignature) + } +} + +private fun MethodNode.buildMethodSignature( + ownerVisibility: ClassVisibility?, + ownerClass: ClassNode +): MethodBinarySignature { + /** + * For getters/setters, pull the annotations from the property + * This is either on the field if any or in a '$annotations' synthetic function. + */ + val annotationHolders = + ownerVisibility?.members?.get(JvmMethodSignature(name, desc))?.propertyAnnotation + val foundAnnotations = ArrayList() + if (annotationHolders != null) { + foundAnnotations += ownerClass.fields.annotationsFor(annotationHolders.field) + foundAnnotations += ownerClass.methods.annotationsFor(annotationHolders.method) + } + + /** + * For synthetic $default methods, pull the annotations from the corresponding method + */ + val alternateDefaultSignature = ownerVisibility?.name?.let { className -> + alternateDefaultSignature(className) + } + foundAnnotations += ownerClass.methods.annotationsFor(alternateDefaultSignature) + + return toMethodBinarySignature(foundAnnotations, alternateDefaultSignature) +} + private fun List.annotationsFor(methodSignature: JvmMethodSignature?): List { if (methodSignature == null) return emptyList() diff --git a/src/test/kotlin/cases/companions/companions.kt b/src/test/kotlin/cases/companions/companions.kt index cb9eb2a4..a5eea596 100644 --- a/src/test/kotlin/cases/companions/companions.kt +++ b/src/test/kotlin/cases/companions/companions.kt @@ -140,3 +140,20 @@ object PrivateInterfaces { } } +@PrivateApi +annotation class PrivateApi + + +class FilteredCompanionObjectHolder private constructor() { + @PrivateApi + companion object { + val F: Int = 42 + } +} + +class FilteredNamedCompanionObjectHolder private constructor() { + @PrivateApi + companion object Named { + val F: Int = 42 + } +} diff --git a/src/test/kotlin/cases/companions/companions.txt b/src/test/kotlin/cases/companions/companions.txt index 21f2417d..9138da81 100644 --- a/src/test/kotlin/cases/companions/companions.txt +++ b/src/test/kotlin/cases/companions/companions.txt @@ -1,3 +1,9 @@ +public final class cases/companions/FilteredCompanionObjectHolder { +} + +public final class cases/companions/FilteredNamedCompanionObjectHolder { +} + public final class cases/companions/InternalClasses { public static final field INSTANCE Lcases/companions/InternalClasses; } diff --git a/src/test/kotlin/tests/CasesPublicAPITest.kt b/src/test/kotlin/tests/CasesPublicAPITest.kt index ae66e059..4fc8955e 100644 --- a/src/test/kotlin/tests/CasesPublicAPITest.kt +++ b/src/test/kotlin/tests/CasesPublicAPITest.kt @@ -27,7 +27,7 @@ class CasesPublicAPITest { @Test fun annotations() { snapshotAPIAndCompare(testName.methodName) } - @Test fun companions() { snapshotAPIAndCompare(testName.methodName) } + @Test fun companions() { snapshotAPIAndCompare(testName.methodName, setOf("cases/companions/PrivateApi")) } @Test fun default() { snapshotAPIAndCompare(testName.methodName) }