Skip to content

Commit

Permalink
Add all Companion class' annotations to corresponding Companion field.
Browse files Browse the repository at this point in the history
Fixes #157
  • Loading branch information
fzhinkin committed Jan 16, 2024
1 parent 07d46e7 commit c1133f0
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 11 deletions.
7 changes: 6 additions & 1 deletion src/main/kotlin/api/KotlinMetadataSignature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,16 @@ 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
if (metadata !is KotlinClassMetadata.Class) return false
return metadata.toKmClass().companionObject == name
}

fun ClassNode.companionName(outerClassMetadata: KotlinClassMetadata?): String {
val outerKClass = (outerClassMetadata as KotlinClassMetadata.Class).toKmClass()
return name + "$" + outerKClass.companionObject
}
33 changes: 24 additions & 9 deletions src/main/kotlin/api/KotlinSignaturesLoading.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,38 @@ public fun Sequence<InputStream>.loadApiFromJvmClasses(visibilityFilter: (String
.map {
val annotationHolders =
mVisibility?.members?.get(JvmFieldSignature(it.name, it.desc))?.propertyAnnotation
val foundAnnotations = methods.annotationsFor(annotationHolders?.method)
it.toFieldBinarySignature(foundAnnotations)
val foundAnnotations = mutableListOf<AnnotationNode>()
foundAnnotations.addAll(methods.annotationsFor(annotationHolders?.method))

var companionClass: ClassNode? = null
if (it.isCompanionField(classNode.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 = companionName(classNode.kotlinMetadata)
companionClass = classNodeMap[companionName]
foundAnnotations.addAll(companionClass?.visibleAnnotations.orEmpty())
foundAnnotations.addAll(companionClass?.invisibleAnnotations.orEmpty())
}

it.toFieldBinarySignature(foundAnnotations) to companionClass
}.filter {
it.isEffectivelyPublic(classAccess, mVisibility)
it.first.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 = it.second ?: return@filter true
val visibility = visibilityMap[companionClass.name] ?: return@filter true
companionClass.isEffectivelyPublic(visibility)
}.map {
it.first
}

// NB: this 'map' is O(methods + properties * methods) which may accidentally be quadratic
Expand Down
17 changes: 17 additions & 0 deletions src/test/kotlin/cases/companions/companions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
6 changes: 6 additions & 0 deletions src/test/kotlin/cases/companions/companions.txt
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/kotlin/tests/CasesPublicAPITest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down

0 comments on commit c1133f0

Please sign in to comment.