Skip to content

Commit

Permalink
Implement basic SkieVisibility configuration.
Browse files Browse the repository at this point in the history
  • Loading branch information
FilipDolnik committed May 15, 2024
1 parent e553fc6 commit 79d42e4
Show file tree
Hide file tree
Showing 16 changed files with 209 additions and 13 deletions.
2 changes: 1 addition & 1 deletion SKIE/acceptance-tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package co.touchlab.skie.configuration.annotations

/**
* Configures visibility of the exported Kotlin declarations in Swift.
* In the future, the visibility will be applied directly to the Obj-C header meaning it will also affect external Obj-C code.
*
* Note: Using multiple visibility annotations on the same declaration results in an undefined behavior, and may result in a compilation crash.
*
* Warning: SKIE does not check whether the configured visibility is correct or not.
* For example, it's possible to create a public interface with internal members.
* Invalid configurations will likely lead to weird issues and compiler crashes.
* In the future, SKIE might introduce explicit checks that will crash the compilation explicitly in those cases.
*/
@Target
annotation class SkieVisibility {

/**
* The declaration will be visible from external modules as usual.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR)
annotation class Public

/**
* The declaration will be visible from external modules, but it will be marked as `swift-private`.
* (Xcode will not include it in autocomplete suggestions.)
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR)
annotation class PublicButHidden

/**
* The declaration will be visible from external modules, but it will be:
* - marked as `swift-private` (Xcode will not include it in autocomplete suggestions.),
* - renamed in Swift by adding the `__` prefix (Obj-C name remains the same)
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class PublicButReplaced

/**
* The declaration will be visible only for declarations within the Kotlin module (including custom Swift code bundled by SKIE).
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR)
annotation class Internal

/**
* The declaration will be visible only for declarations within the Kotlin module (including custom Swift code bundled by SKIE).
* Additionally, the declaration will be renamed in Swift by adding the `__` prefix (Obj-C name remains the same)
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class InternalAndReplaced

/**
* The declaration will either be Public or Internal.
* Which one is chosen depends on whether the declaration is automatically wrapped by SKIE or not.
*
* For example, a top-level function originally exposed as `FileKt.functionName` will be internal, if SKIE generated the global function wrapper for it.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR)
annotation class InternalIfWrapped

/**
* The declaration will not be visible.
*
* Note that this doesn't change whether the declaration is exported by the Kotlin compiler, therefore the compilation overhead from the exported declaration remains unchanged.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR)
annotation class Private
}

Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ interface ConfigurationKey<T> {

interface NonOptional<T : Any> : ConfigurationKey<T> {

fun findAnnotationValue(configurationTarget: ConfigurationTarget): T? = null

override fun hasAnnotationValue(configurationTarget: ConfigurationTarget): kotlin.Boolean =
findAnnotationValue(configurationTarget) != null

fun findAnnotationValue(configurationTarget: ConfigurationTarget): T? = null

override fun getAnnotationValue(configurationTarget: ConfigurationTarget): T =
findAnnotationValue(configurationTarget)
?: throw IllegalStateException("Target $configurationTarget does not have an annotation value.")
Expand Down Expand Up @@ -74,4 +74,15 @@ interface ConfigurationKey<T> {
override fun deserialize(value: kotlin.String?): java.nio.file.Path =
Path(value.throwIfNull())
}

interface Enum<E : kotlin.Enum<E>> : NonOptional<E> {

override fun deserialize(value: kotlin.String?): E =
valueOf(value.throwIfNull())

override fun serialize(value: E): kotlin.String? =
value.name

fun valueOf(value: kotlin.String): E
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlin.reflect.KClass

interface ConfigurationTarget {

fun <T : Annotation> hasAnnotation(kClass: KClass<T>): Boolean
fun hasAnnotation(kClass: KClass<out Annotation>): Boolean

fun <T : Annotation> findAnnotation(kClass: KClass<T>): T?
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package co.touchlab.skie.configuration

import co.touchlab.skie.configuration.annotations.SkieVisibility
import kotlin.reflect.KClass

/**
* Configures visibility of the exported Kotlin declarations in Swift.
* In the future, the visibility will be applied directly to the Obj-C header meaning it will also affect external Obj-C code.
*
* Warning: SKIE does not check whether the configured visibility is correct or not.
* For example, it's possible to create a public interface with internal members.
* Invalid configurations will likely lead to weird issues and compiler crashes.
* In the future, SKIE might introduce explicit checks that will crash the compilation explicitly in those cases.
*/
object SkieVisibility : ConfigurationKey.Enum<co.touchlab.skie.configuration.SkieVisibility.Level>, ConfigurationScope.All {

enum class Level(internal val associatedAnnotation: KClass<out Annotation>) {
/**
* The declaration will be visible from external modules as usual.
*/
Public(SkieVisibility.Public::class),

/**
* The declaration will be visible from external modules, but it will be marked as `swift-private`.
* (Xcode will not include it in autocomplete suggestions.)
*/
PublicButHidden(SkieVisibility.PublicButHidden::class),

/**
* The declaration will be visible from external modules, but it will be:
* - marked as `swift-private` (Xcode will not include it in autocomplete suggestions.),
* - renamed in Swift by adding the `__` prefix (Obj-C name remains the same); constructors are not affected.
*/
PublicButReplaced(SkieVisibility.PublicButReplaced::class),

/**
* The declaration will be visible only for declarations within the Kotlin module (including custom Swift code bundled by SKIE).
*/
Internal(SkieVisibility.Internal::class),

/**
* The declaration will be visible only for declarations within the Kotlin module (including custom Swift code bundled by SKIE).
* Additionally, the declaration will be renamed in Swift by adding the `__` prefix (Obj-C name remains the same); constructors are not affected.
*/
InternalAndReplaced(SkieVisibility.InternalAndReplaced::class),

/**
* The declaration will either be Public or Internal.
* Which one is chosen depends on whether the declaration is automatically wrapped by SKIE or not.
*
* For example, a top-level function originally exposed as `FileKt.functionName` will be internal, if SKIE generated the global function wrapper for it.
*/
InternalIfWrapped(SkieVisibility.InternalIfWrapped::class),

/**
* The declaration will not be visible.
*
* Note that this doesn't change whether the declaration is exported by the Kotlin compiler, therefore the compilation overhead from the exported declaration remains unchanged.
*/
Private(SkieVisibility.Private::class),
}

override val defaultValue: Level = Level.Public

override fun valueOf(value: String): Level = Level.valueOf(value)

override fun findAnnotationValue(configurationTarget: ConfigurationTarget): Level? =
Level.values().firstOrNull { configurationTarget.hasAnnotation(it.associatedAnnotation) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ abstract class SkieConfiguration(
if (isKeySupported(configurationKey)) {
return configurationKey.defaultValue
} else {
error("Configuration key $configurationKey is not supported.")
error("Configuration key $configurationKey was not registered in ConfigurationProvider.")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import co.touchlab.skie.configuration.RootConfiguration
import co.touchlab.skie.configuration.SealedInterop
import co.touchlab.skie.configuration.SimpleFunctionConfiguration
import co.touchlab.skie.configuration.SkieConfiguration
import co.touchlab.skie.configuration.SkieVisibility
import co.touchlab.skie.configuration.SuppressSkieWarning
import co.touchlab.skie.configuration.SuspendInterop
import co.touchlab.skie.configuration.ValueParameterConfiguration
Expand Down Expand Up @@ -51,6 +52,7 @@ class ConfigurationProvider(
SealedInterop.ElseName,
SealedInterop.Case.Visible,
SealedInterop.Case.Name,
SkieVisibility,
SuppressSkieWarning.NameCollision,
SuspendInterop.Enabled,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface IdentifiedConfigurationTarget : ConfigurationTarget {

override val scopeType: KClass<out ConfigurationScope> = ConfigurationScope.Root::class

override fun <T : Annotation> hasAnnotation(kClass: KClass<T>): Boolean = false
override fun hasAnnotation(kClass: KClass<out Annotation>): Boolean = false

override fun <T : Annotation> findAnnotation(kClass: KClass<T>): T? = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import co.touchlab.skie.oir.type.TypeDefOirType
import co.touchlab.skie.oir.type.TypeParameterUsageOirType
import co.touchlab.skie.oir.type.VoidOirType
import co.touchlab.skie.phases.SirPhase
import co.touchlab.skie.phases.util.MustBeExecutedAfterBridgingConfiguration
import co.touchlab.skie.sir.element.SirClass
import co.touchlab.skie.sir.element.SirEnumCase
import co.touchlab.skie.sir.element.SirEnumCaseAssociatedValue
import co.touchlab.skie.sir.element.SirModule
import co.touchlab.skie.sir.element.SirVisibility
import co.touchlab.skie.sir.element.copyTypeParametersFrom

@MustBeExecutedAfterBridgingConfiguration
class SealedEnumGeneratorDelegate(
override val context: SirPhase.Context,
) : SealedGeneratorExtensionContainer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package co.touchlab.skie.phases.features.sealed
import co.touchlab.skie.configuration.SealedInterop
import co.touchlab.skie.kir.element.KirClass
import co.touchlab.skie.phases.SirPhase
import co.touchlab.skie.phases.util.MustBeExecutedAfterBridgingConfiguration
import co.touchlab.skie.sir.element.SirClass
import co.touchlab.skie.sir.element.SirSimpleFunction
import co.touchlab.skie.sir.element.SirTypeParameter
Expand All @@ -17,6 +18,7 @@ import co.touchlab.skie.util.swift.escapeSwiftIdentifier
import io.outfoxx.swiftpoet.CodeBlock
import io.outfoxx.swiftpoet.TypeName

@MustBeExecutedAfterBridgingConfiguration
class SealedFunctionGeneratorDelegate(
override val context: SirPhase.Context,
) : SealedGeneratorExtensionContainer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package co.touchlab.skie.phases.features.sealed
import co.touchlab.skie.configuration.SealedInterop
import co.touchlab.skie.kir.element.KirClass
import co.touchlab.skie.phases.SirPhase
import co.touchlab.skie.phases.util.MustBeExecutedAfterBridgingConfiguration
import co.touchlab.skie.sir.element.SirClass
import co.touchlab.skie.sir.element.isAccessibleFromOtherModules
import co.touchlab.skie.sir.element.superClassType
import co.touchlab.skie.sir.element.toTypeParameterUsage
import co.touchlab.skie.sir.type.SirDeclaredSirType
import co.touchlab.skie.sir.type.SirType
import co.touchlab.skie.sir.type.TypeParameterUsageSirType
import co.touchlab.skie.util.swift.toValidSwiftIdentifier

@MustBeExecutedAfterBridgingConfiguration
interface SealedGeneratorExtensionContainer {

val context: SirPhase.Context
Expand Down Expand Up @@ -48,7 +51,7 @@ interface SealedGeneratorExtensionContainer {
this.visibleSealedSubclasses.isEmpty()

val KirClass.visibleSealedSubclasses: List<KirClass>
get() = this.sealedSubclasses.filter { it.configuration[SealedInterop.Case.Visible] }
get() = this.sealedSubclasses.filter { it.configuration[SealedInterop.Case.Visible] && it.primarySirClass.visibility.isAccessibleFromOtherModules }

fun SirClass.getSealedSubclassType(
enum: SirClass,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import co.touchlab.skie.sir.element.SirSimpleFunction
import co.touchlab.skie.sir.element.SirVisibility
import co.touchlab.skie.sir.element.applyToEntireOverrideHierarchy
import co.touchlab.skie.sir.element.copyValueParametersFrom
import co.touchlab.skie.sir.element.isAccessible
import co.touchlab.skie.sir.element.isAccessibleFromOtherModules
import co.touchlab.skie.sir.element.shallowCopy
import co.touchlab.skie.sir.type.NullableSirType
import co.touchlab.skie.sir.type.OirDeclaredSirType
Expand All @@ -35,6 +37,10 @@ class SwiftSuspendFunctionGenerator {
kotlinBridgingKirFunction = kotlinBridgingKirFunction,
)

if (!bridgeModel.originalFunction.visibility.isAccessible) {
return
}

val extension = sirProvider.getExtension(
classDeclaration = bridgeModel.extensionTypeDeclarationForBridgingFunction,
parent = namespaceProvider.getNamespaceFile(bridgeModel.suspendFunctionOwner),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.touchlab.skie.phases.sir.member

import co.touchlab.skie.configuration.SkieVisibility
import co.touchlab.skie.kir.element.KirCallableDeclaration
import co.touchlab.skie.kir.element.KirClass
import co.touchlab.skie.kir.element.KirConstructor
Expand All @@ -24,12 +25,14 @@ import co.touchlab.skie.sir.element.SirValueParameter
import co.touchlab.skie.sir.element.SirVisibility
import co.touchlab.skie.util.collisionFreeIdentifier
import co.touchlab.skie.util.swift.toValidSwiftIdentifier
import co.touchlab.skie.util.toSirVisibility

class CreateSirMembersPhase(
val context: SirPhase.Context,
) : SirPhase {

private val kirProvider = context.kirProvider
private val sirProvider = context.sirProvider
private val sirTypeTranslator = context.sirTypeTranslator

context(SirPhase.Context)
Expand Down Expand Up @@ -160,10 +163,14 @@ class CreateSirMembersPhase(
}

private val KirCallableDeclaration<*>.visibility: SirVisibility
get() = if (this.isRefinedInSwift) {
SirVisibility.PublicButHidden
} else {
SirVisibility.Public
get() {
val configuredVisibility = this.configuration[SkieVisibility].toSirVisibility()

return if (configuredVisibility == SirVisibility.Public && this.isRefinedInSwift) {
SirVisibility.PublicButHidden
} else {
configuredVisibility
}
}

private val KirCallableDeclaration<*>.isReplaced: Boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.touchlab.skie.phases.sir.type

import co.touchlab.skie.configuration.SkieVisibility
import co.touchlab.skie.kir.element.KirClass
import co.touchlab.skie.kir.element.KirModule
import co.touchlab.skie.oir.element.OirClass
Expand All @@ -8,7 +9,9 @@ import co.touchlab.skie.sir.SirFqName
import co.touchlab.skie.sir.element.SirClass
import co.touchlab.skie.sir.element.SirDeclarationParent
import co.touchlab.skie.sir.element.SirTypeParameter
import co.touchlab.skie.sir.element.SirVisibility
import co.touchlab.skie.sir.element.toSirKind
import co.touchlab.skie.util.toSirVisibility

// Must be class because it has state
class CreateKotlinSirTypesPhase : SirPhase {
Expand All @@ -35,6 +38,7 @@ class CreateKotlinSirTypesPhase : SirPhase {
parent = kirClass.sirParent,
kind = kirClass.oirClass.kind.toSirKind(),
origin = SirClass.Origin.Kir(kirClass),
visibility = kirClass.configuration[SkieVisibility].toSirVisibility(),
)

createTypeParameters(kirClass.oirClass, sirClass)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package co.touchlab.skie.util

import co.touchlab.skie.configuration.SkieVisibility
import co.touchlab.skie.sir.element.SirVisibility

fun SkieVisibility.Level.toSirVisibility(isWrapped: Boolean = false): SirVisibility =
when (this) {
SkieVisibility.Level.Public -> SirVisibility.Public
SkieVisibility.Level.PublicButHidden -> SirVisibility.PublicButHidden
SkieVisibility.Level.PublicButReplaced -> SirVisibility.PublicButHidden
SkieVisibility.Level.Internal -> SirVisibility.Internal
SkieVisibility.Level.InternalAndReplaced -> SirVisibility.Internal
SkieVisibility.Level.InternalIfWrapped -> if (isWrapped) SirVisibility.Internal else SirVisibility.Public
SkieVisibility.Level.Private -> SirVisibility.Private
}
Loading

0 comments on commit 79d42e4

Please sign in to comment.