Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add all Companion class' annotations to corresponding Companion field. #162

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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