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

[WIP] feat: @ZiplineId #1059

Closed
wants to merge 5 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ package app.cash.zipline.api.fir
import app.cash.zipline.kotlin.BridgedInterface.Companion.NON_INTERFACE_FUNCTION_NAMES
import app.cash.zipline.kotlin.FqPackageName
import app.cash.zipline.kotlin.classId
import app.cash.zipline.kotlin.getArgument
import java.io.File
import org.jetbrains.kotlin.fir.FirAnnotationContainer
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.isSupertypeOf
import org.jetbrains.kotlin.fir.analysis.checkers.toRegularClassSymbol
import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.FirFunction
import org.jetbrains.kotlin.fir.declarations.FirProperty
import org.jetbrains.kotlin.fir.declarations.FirRegularClass
import org.jetbrains.kotlin.fir.declarations.getAnnotationByClassId
import org.jetbrains.kotlin.fir.declarations.getStringArgument
import org.jetbrains.kotlin.fir.declarations.utils.isInterface
import org.jetbrains.kotlin.fir.declarations.utils.isSuspend
import org.jetbrains.kotlin.fir.pipeline.FirResult
Expand All @@ -52,6 +56,7 @@ fun readFirZiplineApi(

private val ziplineFqPackage = FqPackageName("app.cash.zipline")
private val ziplineServiceClassId = ziplineFqPackage.classId("ZiplineService")
private val ziplineIdClassId = ziplineFqPackage.classId("ZiplineId")

/**
* Read the frontend intermediate representation of a program and emit its ZiplineService
Expand Down Expand Up @@ -128,16 +133,19 @@ internal class FirZiplineApiReader(
append(valueParameters.joinToString { it.returnTypeRef.asString() })
append("): ${returnTypeRef.asString()}")
}

return FirZiplineFunction(signature)
return getZiplineId()?.let {
FirZiplineFunction(it, signature)
} ?: FirZiplineFunction(signature)
}

private fun FirProperty.asDeclaredZiplineFunction(): FirZiplineFunction {
val signature = when {
isVar -> "var ${symbol.name.identifier}: ${returnTypeRef.asString()}"
else -> "val ${symbol.name.identifier}: ${returnTypeRef.asString()}"
}
return FirZiplineFunction(signature)
return getZiplineId()?.let {
FirZiplineFunction(it, signature)
} ?: FirZiplineFunction(signature)
}

/** See [app.cash.zipline.kotlin.asString]. */
Expand Down Expand Up @@ -177,6 +185,13 @@ internal class FirZiplineApiReader(
}
}

private fun FirAnnotationContainer.getZiplineId(): String? {
return getAnnotationByClassId(
ziplineIdClassId,
session,
)?.getStringArgument(ziplineIdClassId.getArgument("id"))
}

private fun List<FirDeclaration>.findRegularClassesRecursive(): List<FirRegularClass> {
val classes = filterIsInstance<FirRegularClass>()
return classes + classes.flatMap { it.declarations.findRegularClassesRecursive() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrDelegatingConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
Expand All @@ -72,8 +73,12 @@ import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.createDispatchReceiverParameter
import org.jetbrains.kotlin.ir.util.createImplicitParameterDeclarationWithWrappedDescriptor
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction
import org.jetbrains.kotlin.utils.addToStdlib.cast

/** Returns a string as specified by ZiplineFunction.signature. */
internal val IrSimpleFunction.signature: String
Expand Down Expand Up @@ -102,9 +107,22 @@ internal val IrSimpleFunction.signature: String
}
}

@UnsafeCastFunction
private fun <T> IrElement.getConstValue(): T {
return cast<IrConst<T>>().value
}

private val ziplineFqPackageName = FqPackageName("app.cash.zipline")
private val ziplineIdAnnotationFqName = ziplineFqPackageName.classId("ZiplineId").asSingleFqName()

/** Returns a string as specified by ZiplineFunction.id. */
@OptIn(UnsafeCastFunction::class)
internal val IrSimpleFunction.id: String
get() = signature.signatureHash()
get() {

return getAnnotation(ziplineIdAnnotationFqName)?.getValueArgument(0)
?.getConstValue() ?: signature.signatureHash()
}

/** Thrown on invalid or unexpected input code. */
class ZiplineCompilationException(
Expand Down Expand Up @@ -417,3 +435,7 @@ fun getOrCreateCompanion(
enclosing.declarations.add(companionClass)
return companionClass
}

internal fun ClassId.getArgument(argument: String): Name {
return Name.identifier("${asFqNameString()}.$argument")
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package app.cash.zipline.api.fir

import app.cash.zipline.ZiplineId
import app.cash.zipline.ZiplineService
import assertk.assertThat
import assertk.assertions.isEqualTo
Expand All @@ -40,28 +41,37 @@ internal class FirZiplineApiReaderTest {
FirZiplineService(
name = EchoService::class.qualifiedName!!,
functions = listOf(
FirZiplineFunction("annotatedFun", "fun annotatedFun(): kotlin.String"),
FirZiplineFunction("fun close(): kotlin.Unit"),
FirZiplineFunction("fun echo(kotlin.String): kotlin.String"),
FirZiplineFunction("annotatedVal", "val annotatedVal: kotlin.String"),
FirZiplineFunction("val greeting: kotlin.String"),
FirZiplineFunction("annotatedVar", "var annotatedVar: kotlin.String"),
FirZiplineFunction("var terse: kotlin.Boolean"),
),
),
),
FirZiplineService(
name = ExtendedEchoService::class.qualifiedName!!,
functions = listOf(
FirZiplineFunction("annotatedFun", "fun annotatedFun(): kotlin.String"),
FirZiplineFunction("fun close(): kotlin.Unit"),
FirZiplineFunction("fun echo(kotlin.String): kotlin.String"),
FirZiplineFunction("fun echoAll(kotlin.collections.List<kotlin.String>): kotlin.collections.List<kotlin.String>"),
FirZiplineFunction("annotatedVal", "val annotatedVal: kotlin.String"),
FirZiplineFunction("val greeting: kotlin.String"),
FirZiplineFunction("annotatedVar", "var annotatedVar: kotlin.String"),
FirZiplineFunction("var terse: kotlin.Boolean"),
),
),
FirZiplineService(
name = UnnecessaryEchoService::class.qualifiedName!!,
functions = listOf(
FirZiplineFunction("annotatedFunOverride", "fun annotatedFun(): kotlin.String"),
FirZiplineFunction("fun close(): kotlin.Unit"),
FirZiplineFunction("fun echo(kotlin.String): kotlin.String"),
FirZiplineFunction("annotatedValOverride", "val annotatedVal: kotlin.String"),
FirZiplineFunction("val greeting: kotlin.String"),
FirZiplineFunction("annotatedVarOverride", "var annotatedVar: kotlin.String"),
FirZiplineFunction("var terse: kotlin.Boolean"),
),
),
Expand All @@ -74,7 +84,16 @@ internal class FirZiplineApiReaderTest {
interface EchoService : ZiplineService {
val greeting: String
var terse: Boolean

@ZiplineId("annotatedVal")
val annotatedVal: String

@ZiplineId("annotatedVar")
var annotatedVar: String
fun echo(request: String): String

@ZiplineId("annotatedFun")
fun annotatedFun(): String
}

/** This should be included in the output. */
Expand All @@ -84,8 +103,18 @@ internal class FirZiplineApiReaderTest {

/** This should be included in the output, but without additional methods. */
interface UnnecessaryEchoService : EchoService {

@ZiplineId("annotatedValOverride")
override val annotatedVal: String

@ZiplineId("annotatedVarOverride")
override var annotatedVar: String

override fun echo(request: String): String
override fun equals(other: Any?): Boolean

@ZiplineId("annotatedFunOverride")
override fun annotatedFun(): String
}

/** This shouldn't be included in the output. */
Expand All @@ -95,7 +124,13 @@ internal class FirZiplineApiReaderTest {
override var terse: Boolean
get() = error("unexpected call")
set(value) = error("unexpected call")
override val annotatedVal: String
get() = error("unexpected call")
override var annotatedVar: String
get() = error("unexpected call")
set(value) { error("unexpected call") }

override fun echo(request: String) = error("unexpected call")
override fun annotatedFun(): String = error("unexpected call")
}
}
4 changes: 4 additions & 0 deletions zipline/api/android/zipline.api
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ public abstract interface class app/cash/zipline/ZiplineFunction {
public abstract fun isSuspending ()Z
}

public abstract interface annotation class app/cash/zipline/ZiplineId : java/lang/annotation/Annotation {
public abstract fun id ()Ljava/lang/String;
}

public final class app/cash/zipline/ZiplineManifest {
public static final field Companion Lapp/cash/zipline/ZiplineManifest$Companion;
public synthetic fun <init> (ILapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
Expand Down
4 changes: 4 additions & 0 deletions zipline/api/jvm/zipline.api
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ public abstract interface class app/cash/zipline/ZiplineFunction {
public abstract fun isSuspending ()Z
}

public abstract interface annotation class app/cash/zipline/ZiplineId : java/lang/annotation/Annotation {
public abstract fun id ()Ljava/lang/String;
}

public final class app/cash/zipline/ZiplineManifest {
public static final field Companion Lapp/cash/zipline/ZiplineManifest$Companion;
public synthetic fun <init> (ILapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package app.cash.zipline
interface ZiplineFunction<T : ZiplineService> {
/**
* A unique id for this function. By default this is the first 6 bytes of the SHA-256 of the
* function's signature, base64-encoded.
* function's signature, base64-encoded. To provide your own id, annotate your function with [ZiplineId].
*
* These are sample values that correspond to the sample values in [signature].
*
Expand Down
22 changes: 22 additions & 0 deletions zipline/src/commonMain/kotlin/app/cash/zipline/ZiplineId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Cash App
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.zipline

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
// TODO document usage
annotation class ZiplineId(val id: String)
15 changes: 15 additions & 0 deletions zipline/src/jniTest/kotlin/app/cash/zipline/NewServicesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,18 @@ class NewSerializersTest {
suspend fun echo(): @Contextual RequiresContextual
}

@Test
fun ziplineIdIsAppliedToGeneratedService() {
val function =
onlyZiplineFunction(serviceSerializer = ziplineServiceSerializer<SimpleZiplineService>()) as ZiplineFunction<*>

assertEquals(function.id, testZiplineId)
}
interface SimpleZiplineService : ZiplineService {
@ZiplineId(testZiplineId)
fun annotatedFunction()
}

private fun <T> suspendCallbackSerializer(
resultSerializer: KSerializer<T>,
): KSerializer<SuspendCallback<T>> {
Expand Down Expand Up @@ -475,4 +487,7 @@ class NewSerializersTest {
json.decodeFromString(actual, json.encodeToString(expected, sampleValue)),
)
}
companion object {
private const val testZiplineId = "TEST_ZIPLINE_ID"
}
}