From 5d66ba57729768f883a7216a0d9d98264f3a918d Mon Sep 17 00:00:00 2001 From: Dewan Tawsif Date: Tue, 11 Feb 2025 02:32:12 +0600 Subject: [PATCH] Do not repeat @OptIn annotation for generated class (#773) * Do not repeat @OptIn annotation for generated class * Add test for opt in marker class deduplication --- docs/CHANGELOG.md | 1 + .../ktorfit/poetspec/ImplClassSpec2.kt | 36 ++++++++---- .../de/jensklingenberg/ktorfit/OptInTest.kt | 58 ++++++++++++++++--- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7529d270..d43d687c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,6 +23,7 @@ ktorfit{ ## Fixed - @Headers annotation produces unexpected newline in generated code by ksp plugin #752 +- Generated code containing repeated @OptIn annotation #767 # [2.2.0]() * Supported Kotlin version: 2.0.0; 2.0.10; 2.0.20, 2.1.0-Beta1; 2.0.21-RC, 2.0.21, 2.1.0-RC, 2.1.0-RC2, 2.1.0, 2.1.10 diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt index 5ef606c7..403ce981 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt @@ -1,7 +1,9 @@ package de.jensklingenberg.ktorfit.poetspec import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSTypeReference import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName @@ -10,7 +12,7 @@ import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName import de.jensklingenberg.ktorfit.KtorfitOptions import de.jensklingenberg.ktorfit.model.ClassData @@ -56,8 +58,6 @@ private fun createImplClassTypeSpec( implClassProperties: List, funSpecs: List ): TypeSpec { - val optInAnnotations = - classData.annotations.filter { it.shortName.getShortName() == "OptIn" }.map { it.toAnnotationSpec() } val helperProperty = PropertySpec .builder(converterHelper.objectName, converterHelper.toClassName()) @@ -65,16 +65,9 @@ private fun createImplClassTypeSpec( .addModifiers(KModifier.PRIVATE) .build() - val internalApiAnnotation = - AnnotationSpec - .builder(ClassName("kotlin", "OptIn")) - .addMember( - "%T::class", - internalApi, - ).build() return TypeSpec .classBuilder(implClassName) - .addAnnotations(optInAnnotations + internalApiAnnotation) + .addAnnotation(getOptInAnnotation(classData.annotations)) .addModifiers(classData.modifiers) .primaryConstructor( FunSpec @@ -94,6 +87,27 @@ private fun createImplClassTypeSpec( .build() } +private fun getOptInAnnotation(annotations: List): AnnotationSpec { + val markerClasses = + annotations + .filter { it.shortName.getShortName() == "OptIn" } + .flatMap { annotation -> + @Suppress("UNCHECKED_CAST") + (annotation.arguments[0].value as? List) + ?.map { it.toClassName() } + .orEmpty() + } + .plus(internalApi) + .toTypedArray() + + val format = (1..markerClasses.size).joinToString { "%T::class" } + + return AnnotationSpec + .builder(ClassName("kotlin", "OptIn")) + .addMember(format, *markerClasses) + .build() +} + private fun propertySpec(property: KSPropertyDeclaration): PropertySpec { val propBuilder = PropertySpec diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/OptInTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/OptInTest.kt index 23b273ba..0a950d71 100644 --- a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/OptInTest.kt +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/OptInTest.kt @@ -21,17 +21,16 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @OptIn(ExperimentalCompilerApi::class) interface TestService { -@Headers(value = ["x:y","a:b"]) -@GET("posts") -@OptIn(ExperimentalCompilerApi::class) -suspend fun test(@Header("testHeader") testParameterNonNullable: String?, @Header("testHeader") testParameterNullable: String?, @HeaderMap("testHeaderMap") testParameter2: Map): String + @Headers(value = ["x:y","a:b"]) + @GET("posts") + @OptIn(ExperimentalCompilerApi::class) + suspend fun test(@Header("testHeader") testParameterNonNullable: String?, @Header("testHeader") testParameterNullable: String?, @HeaderMap("testHeaderMap") testParameter2: Map): String } """, ) val expectedHeadersArgumentText = - """@OptIn(ExperimentalCompilerApi::class) -@OptIn(InternalKtorfitApi::class) + """@OptIn(ExperimentalCompilerApi::class, InternalKtorfitApi::class) public class _TestServiceImpl( private val _ktorfit: Ktorfit, ) : TestService { @@ -41,9 +40,52 @@ public class _TestServiceImpl( override suspend fun test(""" val compilation = getCompilation(listOf(source)) - val result = compilation.compile() - val generatedSourcesDir = compilation.kspSourcesDir + val generatedSourcesDir = compilation.apply { compile() }.kspSourcesDir + val generatedFile = + File( + generatedSourcesDir, + "/kotlin/com/example/api/_TestServiceImpl.kt", + ) + + val actualSource = generatedFile.readText() + assertTrue(actualSource.contains(expectedHeadersArgumentText)) + } + + @Test + fun `when OptIn annotation are add to the implementation class do not repeat marker classes`() { + val source = + SourceFile.kotlin( + "Source.kt", + """package com.example.api +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Headers +import de.jensklingenberg.ktorfit.http.Header +import de.jensklingenberg.ktorfit.http.HeaderMap +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +@OptIn(ExperimentalCompilerApi::class) +interface TestService { + @GET("posts") + @OptIn(ExperimentalCompilerApi::class) + suspend fun test1() + + @GET("posts") + @OptIn(ExperimentalCompilerApi::class) + suspend fun test2() +} + """, + ) + + val expectedHeadersArgumentText = + """@OptIn(ExperimentalCompilerApi::class, InternalKtorfitApi::class) +public class _TestServiceImpl( + private val _ktorfit: Ktorfit, +) : TestService {""" + + val compilation = getCompilation(listOf(source)) + + val generatedSourcesDir = compilation.apply { compile() }.kspSourcesDir val generatedFile = File( generatedSourcesDir,