Skip to content

Commit

Permalink
Add all Companion class annotations to the corresponding Companion fi…
Browse files Browse the repository at this point in the history
…eld. (#162)

* Add all Companion class annotations to the corresponding Companion field.
* Extract logic for building field and method signatures to separate methods

Fixes #157
  • Loading branch information
fzhinkin committed Jan 18, 2024
1 parent 07d46e7 commit 066e01e
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 42 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
}

internal fun ClassNode.companionName(outerClassMetadata: KotlinClassMetadata?): String {
val outerKClass = (outerClassMetadata as KotlinClassMetadata.Class).toKmClass()
return name + "$" + outerKClass.companionObject
}
127 changes: 87 additions & 40 deletions src/main/kotlin/api/KotlinSignaturesLoading.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,54 +43,25 @@ public fun Sequence<InputStream>.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<AnnotationNode>()
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,
Expand All @@ -102,6 +73,82 @@ public fun Sequence<InputStream>.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<String, ClassNode>
): FieldBinarySignatureWrapper {
val annotationHolders =
ownerVisibility?.members?.get(JvmFieldSignature(name, desc))?.propertyAnnotation
val foundAnnotations = mutableListOf<AnnotationNode>()
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<AnnotationNode>()
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<MethodNode>.annotationsFor(methodSignature: JvmMethodSignature?): List<AnnotationNode> {
if (methodSignature == null) return emptyList()

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 066e01e

Please sign in to comment.